chore(config): 添加项目配置文件和隐私协议
- 添加 .editorconfig 文件统一代码风格 - 添加 .env.development 和 .env.example 环境配置文件 - 添加 .eslintignore 和 .eslintrc.js 代码检查配置 - 添加 .gitignore 版本控制忽略文件配置 - 添加 .prettierignore 格式化忽略配置 - 添加隐私协议HTML文件 - 添加API密钥管理组件基础结构
This commit is contained in:
71
src/views/shop/dashboard/components/search.vue
Normal file
71
src/views/shop/dashboard/components/search.vue
Normal 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>
|
||||
408
src/views/shop/dashboard/components/websiteEdit.vue
Normal file
408
src/views/shop/dashboard/components/websiteEdit.vue
Normal 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>
|
||||
316
src/views/shop/dashboard/index.vue
Normal file
316
src/views/shop/dashboard/index.vue
Normal file
@@ -0,0 +1,316 @@
|
||||
<template>
|
||||
<a-page-header :show-back="false">
|
||||
<a-row :gutter="16">
|
||||
<!-- 应用基本信息卡片 -->
|
||||
<a-col :span="24" style="margin-bottom: 16px">
|
||||
<a-card title="概况" :bordered="false">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="6">
|
||||
<a-image
|
||||
:width="80"
|
||||
:height="80"
|
||||
:preview="false"
|
||||
style="border-radius: 8px"
|
||||
:src="siteStore.logo"
|
||||
fallback="/logo.png"
|
||||
/>
|
||||
</a-col>
|
||||
<a-col :span="14">
|
||||
<div class="system-info">
|
||||
<h2
|
||||
class="ele-text-heading cursor-pointer"
|
||||
@click="$router.push('/website/index')"
|
||||
>{{ siteStore.appName }}</h2
|
||||
>
|
||||
<p class="ele-text-secondary">{{ siteStore.description }}</p>
|
||||
<a-space>
|
||||
<a-tag color="green">{{ siteStore.statusText }}</a-tag>
|
||||
<a-tag color="blue" v-if="siteStore.version">{{
|
||||
siteStore.version
|
||||
}}</a-tag>
|
||||
<a-popover title="小程序码" v-if="siteStore.mpQrCode">
|
||||
<template #content>
|
||||
<p
|
||||
><img
|
||||
:src="siteStore.mpQrCode"
|
||||
alt="小程序码"
|
||||
width="300"
|
||||
height="300"
|
||||
/></p>
|
||||
</template>
|
||||
<a-tag>
|
||||
<QrcodeOutlined />
|
||||
</a-tag>
|
||||
</a-popover>
|
||||
</a-space>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="3">
|
||||
<div class="flex justify-center items-center h-full w-full">
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<!-- 统计数据卡片 -->
|
||||
<a-col :span="6">
|
||||
<a-card :bordered="false" class="stat-card">
|
||||
<a-statistic
|
||||
title="用户总数"
|
||||
:value="userCount"
|
||||
:value-style="{ color: '#3f8600' }"
|
||||
:loading="loading"
|
||||
>
|
||||
<template #prefix>
|
||||
<UserOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="6">
|
||||
<a-card :bordered="false" class="stat-card">
|
||||
<a-statistic
|
||||
title="订单总数"
|
||||
:value="orderCount"
|
||||
:value-style="{ color: '#1890ff' }"
|
||||
:loading="loading"
|
||||
>
|
||||
<template #prefix>
|
||||
<AccountBookOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="6">
|
||||
<a-card :bordered="false" class="stat-card">
|
||||
<a-statistic
|
||||
title="总营业额"
|
||||
:value="totalSales"
|
||||
:value-style="{ color: '#cf1322' }"
|
||||
:loading="loading"
|
||||
>
|
||||
<template #prefix>
|
||||
<MoneyCollectOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="6">
|
||||
<a-card :bordered="false" class="stat-card">
|
||||
<a-statistic
|
||||
title="系统运行天数"
|
||||
:value="runDays"
|
||||
suffix="天"
|
||||
:value-style="{ color: '#722ed1' }"
|
||||
:loading="loading"
|
||||
>
|
||||
<template #prefix>
|
||||
<ClockCircleOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<!-- 系统基本信息 -->
|
||||
<a-col :span="12">
|
||||
<a-card title="基本信息" :bordered="false">
|
||||
<a-descriptions :column="1" size="small">
|
||||
<a-descriptions-item label="系统名称">
|
||||
{{ systemInfo.name }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="版本号">
|
||||
{{ systemInfo.version }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="部署环境">
|
||||
{{ systemInfo.environment }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="数据库">
|
||||
{{ systemInfo.database }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="服务器">
|
||||
{{ systemInfo.server }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="创建时间">
|
||||
{{ siteInfo?.createTime }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="到期时间">
|
||||
{{ siteInfo?.expirationTime }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="技术支持">
|
||||
<span
|
||||
class="cursor-pointer"
|
||||
@click="openNew(`https://websoft.top`)"
|
||||
>网宿软件</span
|
||||
>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<!-- 快捷操作 -->
|
||||
<a-col :span="12">
|
||||
<a-card title="快捷操作" :bordered="false" style="min-height: 353px">
|
||||
<a-space direction="vertical" style="width: 100%">
|
||||
<a-button
|
||||
type="primary"
|
||||
block
|
||||
@click="$router.push('/website/field')"
|
||||
:loading="loading"
|
||||
>
|
||||
<UngroupOutlined />
|
||||
参数配置
|
||||
</a-button>
|
||||
<a-button block @click="$router.push('/shopOrder')">
|
||||
<CalendarOutlined />
|
||||
订单管理
|
||||
</a-button>
|
||||
<a-button block @click="$router.push('/system/user')">
|
||||
<UserOutlined />
|
||||
用户管理
|
||||
</a-button>
|
||||
<a-button block @click="$router.push('/website/index')">
|
||||
<ShopOutlined />
|
||||
站点管理
|
||||
</a-button>
|
||||
<!-- <a-button block @click="refreshStatistics" :loading="loading">-->
|
||||
<!-- <ReloadOutlined/>-->
|
||||
<!-- 刷新统计-->
|
||||
<!-- </a-button>-->
|
||||
<a-button block @click="$router.push('/system/login-record')">
|
||||
<FileTextOutlined />
|
||||
登录日志
|
||||
</a-button>
|
||||
<a-button block @click="clearSiteInfoCache">
|
||||
<ClearOutlined />
|
||||
清除缓存
|
||||
</a-button>
|
||||
<!-- <a-button block @click="$router.push('/system/setting')">-->
|
||||
<!-- <SettingOutlined/>-->
|
||||
<!-- 系统设置-->
|
||||
<!-- </a-button>-->
|
||||
</a-space>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-page-header>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue';
|
||||
import {
|
||||
UserOutlined,
|
||||
CalendarOutlined,
|
||||
QrcodeOutlined,
|
||||
ShopOutlined,
|
||||
ClockCircleOutlined,
|
||||
AccountBookOutlined,
|
||||
FileTextOutlined,
|
||||
ClearOutlined,
|
||||
UngroupOutlined,
|
||||
MoneyCollectOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
import { message } from 'ant-design-vue/es';
|
||||
import { openNew } from '@/utils/common';
|
||||
import { useSiteStore } from '@/store/modules/site';
|
||||
import { useStatisticsStore } from '@/store/modules/statistics';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { removeSiteInfoCache } from '@/api/cms/cmsWebsite';
|
||||
|
||||
// 使用状态管理
|
||||
const siteStore = useSiteStore();
|
||||
const statisticsStore = useStatisticsStore();
|
||||
|
||||
// 从 store 中获取响应式数据
|
||||
const { siteInfo, loading: siteLoading } = storeToRefs(siteStore);
|
||||
const { loading: statisticsLoading } = storeToRefs(statisticsStore);
|
||||
|
||||
// 系统信息
|
||||
const systemInfo = ref({
|
||||
name: '小程序开发',
|
||||
description:
|
||||
'基于Spring、SpringBoot、SpringMVC等技术栈构建的前后端分离开发平台',
|
||||
version: '2.0.0',
|
||||
status: '运行中',
|
||||
logo: '/logo.png',
|
||||
environment: '生产环境',
|
||||
database: 'MySQL 8.0',
|
||||
server: 'Linux CentOS 7.9',
|
||||
expirationTime: '2024-01-01 09:00:00'
|
||||
});
|
||||
|
||||
// 计算属性
|
||||
const runDays = computed(() => siteStore.runDays);
|
||||
const userCount = computed(() => statisticsStore.userCount);
|
||||
const orderCount = computed(() => statisticsStore.orderCount);
|
||||
const totalSales = computed(() => statisticsStore.totalSales);
|
||||
|
||||
// 加载状态
|
||||
const loading = computed(() => siteLoading.value || statisticsLoading.value);
|
||||
|
||||
// 清除缓存
|
||||
const clearSiteInfoCache = () => {
|
||||
removeSiteInfoCache('SiteInfo:' + localStorage.getItem('TenantId')).then(
|
||||
(msg) => {
|
||||
if (msg) {
|
||||
message.success(msg);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// 刷新统计数据
|
||||
const refreshStatistics = async () => {
|
||||
try {
|
||||
await statisticsStore.forceRefresh();
|
||||
message.success('统计数据刷新成功');
|
||||
} catch (error) {
|
||||
console.error('刷新统计数据失败:', error);
|
||||
message.error('刷新统计数据失败');
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
// 加载网站信息和统计数据
|
||||
try {
|
||||
await Promise.all([
|
||||
siteStore.fetchSiteInfo(),
|
||||
statisticsStore.fetchStatistics()
|
||||
]);
|
||||
|
||||
// 开始自动刷新统计数据(每5分钟)
|
||||
statisticsStore.startAutoRefresh();
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// 组件卸载时停止自动刷新
|
||||
statisticsStore.stopAutoRefresh();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.system-info h2 {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
text-align: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.stat-card :deep(.ant-statistic-title) {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.stat-card :deep(.ant-statistic-content) {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
345
src/views/shop/shopAdmin/components/invitation-modal.vue
Normal file
345
src/views/shop/shopAdmin/components/invitation-modal.vue
Normal 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>
|
||||
39
src/views/shop/shopAdmin/components/org-select.vue
Normal file
39
src/views/shop/shopAdmin/components/org-select.vue
Normal 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>
|
||||
42
src/views/shop/shopAdmin/components/search.vue
Normal file
42
src/views/shop/shopAdmin/components/search.vue
Normal 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>
|
||||
42
src/views/shop/shopAdmin/components/super-admin.vue
Normal file
42
src/views/shop/shopAdmin/components/super-admin.vue
Normal 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>
|
||||
275
src/views/shop/shopAdmin/components/user-edit.vue
Normal file
275
src/views/shop/shopAdmin/components/user-edit.vue
Normal 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>
|
||||
439
src/views/shop/shopAdmin/index.vue
Normal file
439
src/views/shop/shopAdmin/index.vue
Normal 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>
|
||||
42
src/views/shop/shopCoupon/components/search.vue
Normal file
42
src/views/shop/shopCoupon/components/search.vue
Normal 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>
|
||||
958
src/views/shop/shopCoupon/components/shopCouponEdit.vue
Normal file
958
src/views/shop/shopCoupon/components/shopCouponEdit.vue
Normal file
@@ -0,0 +1,958 @@
|
||||
<!-- 编辑弹窗 -->
|
||||
<template>
|
||||
<ele-modal
|
||||
:width="1200"
|
||||
:visible="visible"
|
||||
:maskClosable="false"
|
||||
:maxable="maxable"
|
||||
:title="isUpdate ? '编辑优惠券' : '新增优惠券'"
|
||||
:body-style="{ paddingBottom: '28px' }"
|
||||
@update:visible="updateVisible"
|
||||
@ok="save"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
>
|
||||
<!-- 基本信息 -->
|
||||
<a-divider orientation="left">
|
||||
<span style="color: #1890ff; font-weight: 600">基本信息</span>
|
||||
</a-divider>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="优惠券名称" name="name">
|
||||
<a-input placeholder="请输入优惠券名称" v-model:value="form.name" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="优惠券类型" name="type">
|
||||
<a-select
|
||||
v-model:value="form.type"
|
||||
placeholder="请选择优惠券类型"
|
||||
@change="onTypeChange"
|
||||
>
|
||||
<a-select-option :value="10">
|
||||
<div class="coupon-type-option">
|
||||
<a-tag color="red">满减券</a-tag>
|
||||
<span>满足条件减免金额</span>
|
||||
</div>
|
||||
</a-select-option>
|
||||
<a-select-option :value="20">
|
||||
<div class="coupon-type-option">
|
||||
<a-tag color="orange">折扣券</a-tag>
|
||||
<span>按比例折扣</span>
|
||||
</div>
|
||||
</a-select-option>
|
||||
<a-select-option :value="30">
|
||||
<div class="coupon-type-option">
|
||||
<a-tag color="green">免费券</a-tag>
|
||||
<span>免费使用</span>
|
||||
</div>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="优惠券描述" name="description">
|
||||
<a-textarea
|
||||
placeholder="请输入优惠券描述"
|
||||
v-model:value="form.description"
|
||||
:rows="3"
|
||||
:maxlength="200"
|
||||
show-count
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 优惠设置 -->
|
||||
<a-divider orientation="left">
|
||||
<span style="color: #1890ff; font-weight: 600">优惠设置</span>
|
||||
</a-divider>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="最低消费金额" name="minPrice">
|
||||
<a-input-number
|
||||
:min="0"
|
||||
:precision="2"
|
||||
placeholder="请输入最低消费金额"
|
||||
v-model:value="form.minPrice"
|
||||
style="width: 100%"
|
||||
>
|
||||
<template #addonAfter>元</template>
|
||||
</a-input-number>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<!-- 满减券设置 -->
|
||||
<a-form-item
|
||||
v-if="form.type === 10"
|
||||
label="减免金额"
|
||||
name="reducePrice"
|
||||
>
|
||||
<a-input-number
|
||||
:min="0"
|
||||
:precision="2"
|
||||
placeholder="请输入减免金额"
|
||||
v-model:value="form.reducePrice"
|
||||
style="width: 100%"
|
||||
>
|
||||
<template #addonAfter>元</template>
|
||||
</a-input-number>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 折扣券设置 -->
|
||||
<a-form-item v-if="form.type === 20" label="折扣率" name="discount">
|
||||
<a-input-number
|
||||
:min="0.1"
|
||||
:max="99.9"
|
||||
:precision="1"
|
||||
placeholder="请输入折扣率"
|
||||
v-model:value="form.discount"
|
||||
style="width: 100%"
|
||||
>
|
||||
<template #addonAfter>折</template>
|
||||
</a-input-number>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 有效期设置 -->
|
||||
<a-divider orientation="left">
|
||||
<span style="color: #1890ff; font-weight: 600">有效期设置</span>
|
||||
</a-divider>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="到期类型" name="expireType">
|
||||
<a-radio-group
|
||||
v-model:value="form.expireType"
|
||||
@change="onExpireTypeChange"
|
||||
>
|
||||
<a-radio :value="10">
|
||||
<a-tag color="blue">领取后生效</a-tag>
|
||||
<span style="margin-left: 8px">用户领取后开始计时</span>
|
||||
</a-radio>
|
||||
<a-radio :value="20">
|
||||
<a-tag color="purple">固定时间</a-tag>
|
||||
<span style="margin-left: 8px">指定有效期时间段</span>
|
||||
</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
v-if="form.expireType === 10"
|
||||
label="有效天数"
|
||||
name="expireDay"
|
||||
>
|
||||
<a-input-number
|
||||
:min="1"
|
||||
placeholder="请输入有效天数"
|
||||
v-model:value="form.expireDay"
|
||||
style="width: 100%"
|
||||
>
|
||||
<template #addonAfter>天</template>
|
||||
</a-input-number>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<div v-if="form.expireType === 20" class="fixed-time-config">
|
||||
<a-alert
|
||||
message="固定时间配置"
|
||||
description="设置优惠券的固定有效期时间段"
|
||||
type="info"
|
||||
show-icon
|
||||
style="margin-bottom: 16px"
|
||||
/>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="有效期开始时间" name="startTime">
|
||||
<a-date-picker
|
||||
placeholder="请选择开始时间"
|
||||
v-model:value="form.startTime"
|
||||
style="width: 100%"
|
||||
show-time
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="有效期结束时间" name="endTime">
|
||||
<a-date-picker
|
||||
placeholder="请选择结束时间"
|
||||
v-model:value="form.endTime"
|
||||
style="width: 100%"
|
||||
show-time
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<!-- 适用范围 -->
|
||||
<a-divider orientation="left">
|
||||
<span style="color: #1890ff; font-weight: 600">适用范围</span>
|
||||
</a-divider>
|
||||
|
||||
<a-form-item label="适用范围" name="applyRange">
|
||||
<a-radio-group
|
||||
v-model:value="form.applyRange"
|
||||
@change="onApplyRangeChange"
|
||||
>
|
||||
<a-radio :value="10">
|
||||
<a-tag color="cyan">全部商品</a-tag>
|
||||
<span style="margin-left: 8px">适用于所有商品</span>
|
||||
</a-radio>
|
||||
<a-radio :value="20">
|
||||
<a-tag color="geekblue">指定商品</a-tag>
|
||||
<span style="margin-left: 8px">仅适用于指定商品</span>
|
||||
</a-radio>
|
||||
<a-radio :value="30">
|
||||
<a-tag color="purple">指定分类</a-tag>
|
||||
<span style="margin-left: 8px">适用于指定商品分类</span>
|
||||
</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<div v-if="form.applyRange === 30" class="apply-range-config">
|
||||
<a-alert
|
||||
message="分类限制配置"
|
||||
description="选择优惠券适用的商品分类"
|
||||
type="info"
|
||||
show-icon
|
||||
style="margin-bottom: 16px"
|
||||
/>
|
||||
<a-form-item label="限制分类">
|
||||
<a-cascader
|
||||
v-model:value="applyCateListValue"
|
||||
:options="goodsCateList"
|
||||
multiple
|
||||
placeholder="请选择商品分类"
|
||||
:fieldNames="{ label: 'title', value: 'categoryId' }"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</div>
|
||||
|
||||
<div v-if="form.applyRange === 20" class="apply-range-config">
|
||||
<a-alert
|
||||
message="商品限制配置"
|
||||
description="选择优惠券适用的具体商品"
|
||||
type="info"
|
||||
show-icon
|
||||
style="margin-bottom: 16px"
|
||||
/>
|
||||
<a-form-item label="限制商品">
|
||||
<a-select
|
||||
mode="multiple"
|
||||
v-model:value="applyItemListValue"
|
||||
placeholder="请选择商品"
|
||||
style="width: 100%"
|
||||
:filter-option="false"
|
||||
:show-search="true"
|
||||
@search="searchGoods"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="(item, index) in goodsList"
|
||||
:key="index"
|
||||
:value="item.goodsId"
|
||||
>
|
||||
{{ item.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 发放设置 -->
|
||||
<a-divider orientation="left">
|
||||
<span style="color: #1890ff; font-weight: 600">发放设置</span>
|
||||
</a-divider>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="发放总数量" name="totalCount">
|
||||
<a-input-number
|
||||
:min="-1"
|
||||
placeholder="请输入发放总数量"
|
||||
v-model:value="form.totalCount"
|
||||
style="width: 100%"
|
||||
>
|
||||
<template #addonAfter>张</template>
|
||||
</a-input-number>
|
||||
<div style="color: #999; font-size: 12px; margin-top: 4px">
|
||||
-1 表示无限制
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="每人限领数量" name="limitPerUser">
|
||||
<a-input-number
|
||||
:min="-1"
|
||||
placeholder="请输入每人限领数量"
|
||||
v-model:value="form.limitPerUser"
|
||||
style="width: 100%"
|
||||
>
|
||||
<template #addonAfter>张</template>
|
||||
</a-input-number>
|
||||
<div style="color: #999; font-size: 12px; margin-top: 4px">
|
||||
-1 表示无限制
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 状态设置 -->
|
||||
<a-divider orientation="left">
|
||||
<span style="color: #1890ff; font-weight: 600">状态设置</span>
|
||||
</a-divider>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="启用状态" name="enabled">
|
||||
<a-switch
|
||||
v-model:checked="form.enabled"
|
||||
:checked-value="1"
|
||||
:un-checked-value="0"
|
||||
checked-children="启用"
|
||||
un-checked-children="禁用"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="显示状态" name="status">
|
||||
<a-switch
|
||||
v-model:checked="form.status"
|
||||
:checked-value="0"
|
||||
:un-checked-value="1"
|
||||
checked-children="显示"
|
||||
un-checked-children="隐藏"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="排序" name="sortNumber">
|
||||
<a-input-number
|
||||
:min="0"
|
||||
placeholder="数字越小越靠前"
|
||||
v-model:value="form.sortNumber"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 优惠券预览 -->
|
||||
<div class="coupon-preview" v-if="form.name && form.type">
|
||||
<a-divider orientation="left">
|
||||
<span style="color: #1890ff; font-weight: 600">优惠券预览</span>
|
||||
</a-divider>
|
||||
<div class="coupon-card">
|
||||
<div class="coupon-header">
|
||||
<div class="coupon-type">
|
||||
<a-tag :color="getCouponTypeColor()">{{
|
||||
getCouponTypeName()
|
||||
}}</a-tag>
|
||||
</div>
|
||||
<div class="coupon-value">
|
||||
{{ getCouponValueText() }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="coupon-body">
|
||||
<div class="coupon-name">{{ form.name }}</div>
|
||||
<div class="coupon-desc">{{ form.description || '暂无描述' }}</div>
|
||||
<div class="coupon-condition">
|
||||
满{{ form.minPrice || 0 }}元可用
|
||||
</div>
|
||||
</div>
|
||||
<div class="coupon-footer">
|
||||
<div class="coupon-expire">
|
||||
{{ getExpireText() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-form>
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { Form, message } from 'ant-design-vue';
|
||||
import { assignObject } from 'ele-admin-pro';
|
||||
import { addShopCoupon, updateShopCoupon } from '@/api/shop/shopCoupon';
|
||||
import { ShopCoupon } from '@/api/shop/shopCoupon/model';
|
||||
import { FormInstance } from 'ant-design-vue/es/form';
|
||||
import { ShopGoods } from '@/api/shop/shopGoods/model';
|
||||
import { listShopGoods } from '@/api/shop/shopGoods';
|
||||
import { ShopGoodsCategory } from '@/api/shop/shopGoodsCategory/model';
|
||||
import { listShopGoodsCategory } from '@/api/shop/shopGoodsCategory';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
// 是否是修改
|
||||
const isUpdate = ref(false);
|
||||
const useForm = Form.useForm;
|
||||
|
||||
const props = defineProps<{
|
||||
// 弹窗是否打开
|
||||
visible: boolean;
|
||||
// 修改回显的数据
|
||||
data?: ShopCoupon | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
// 提交状态
|
||||
const loading = ref(false);
|
||||
// 是否显示最大化切换按钮
|
||||
const maxable = ref(true);
|
||||
// 表格选中数据
|
||||
const formRef = ref<FormInstance | null>(null);
|
||||
|
||||
// 表单数据
|
||||
const form = reactive<ShopCoupon>({
|
||||
id: undefined,
|
||||
name: '',
|
||||
description: '',
|
||||
type: 10,
|
||||
reducePrice: undefined,
|
||||
discount: undefined,
|
||||
minPrice: 0,
|
||||
expireType: 10,
|
||||
expireDay: 30,
|
||||
startTime: undefined,
|
||||
endTime: undefined,
|
||||
applyRange: 10,
|
||||
applyRangeConfig: '',
|
||||
isExpire: 0,
|
||||
sortNumber: 100,
|
||||
status: 0,
|
||||
deleted: 0,
|
||||
userId: undefined,
|
||||
tenantId: undefined,
|
||||
createTime: undefined,
|
||||
updateTime: undefined,
|
||||
totalCount: -1,
|
||||
issuedCount: 0,
|
||||
limitPerUser: 1,
|
||||
enabled: 1
|
||||
});
|
||||
|
||||
// 商品分类列表
|
||||
const goodsCateList = ref<ShopGoodsCategory[]>([]);
|
||||
// 商品列表
|
||||
const goodsList = ref<ShopGoods[]>([]);
|
||||
// 适用分类值
|
||||
const applyCateListValue = ref<number[]>([]);
|
||||
// 适用商品值
|
||||
const applyItemListValue = ref<number[]>([]);
|
||||
|
||||
/* 更新visible */
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive({
|
||||
name: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入优惠券名称',
|
||||
trigger: 'blur'
|
||||
},
|
||||
{
|
||||
min: 2,
|
||||
max: 50,
|
||||
message: '优惠券名称长度应在2-50个字符之间',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
type: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择优惠券类型',
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
reducePrice: [
|
||||
{
|
||||
validator: (rule: any, value: any) => {
|
||||
if (form.type === 10 && (!value || value <= 0)) {
|
||||
return Promise.reject('满减券减免金额必须大于0');
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
discount: [
|
||||
{
|
||||
validator: (rule: any, value: any) => {
|
||||
if (form.type === 20 && (!value || value <= 0 || value >= 100)) {
|
||||
return Promise.reject('折扣率必须在0-100之间');
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
minPrice: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入最低消费金额',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
expireType: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择到期类型',
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
expireDay: [
|
||||
{
|
||||
validator: (rule: any, value: any) => {
|
||||
if (form.expireType === 10 && (!value || value <= 0)) {
|
||||
return Promise.reject('有效天数必须大于0');
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
startTime: [
|
||||
{
|
||||
validator: (rule: any, value: any) => {
|
||||
if (form.expireType === 20 && !value) {
|
||||
return Promise.reject('请选择有效期开始时间');
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
endTime: [
|
||||
{
|
||||
validator: (rule: any, value: any) => {
|
||||
if (form.expireType === 20 && !value) {
|
||||
return Promise.reject('请选择有效期结束时间');
|
||||
}
|
||||
if (
|
||||
form.expireType === 20 &&
|
||||
value &&
|
||||
form.startTime &&
|
||||
dayjs(value).isBefore(dayjs(form.startTime))
|
||||
) {
|
||||
return Promise.reject('结束时间不能早于开始时间');
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
totalCount: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入发放总数量',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
limitPerUser: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入每人限领数量',
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
/* 优惠券类型改变 */
|
||||
const onTypeChange = (value: number) => {
|
||||
// 清空相关字段
|
||||
if (value !== 10) {
|
||||
form.reducePrice = undefined;
|
||||
}
|
||||
if (value !== 20) {
|
||||
form.discount = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
/* 到期类型改变 */
|
||||
const onExpireTypeChange = (e: any) => {
|
||||
const value = e.target.value;
|
||||
if (value === 10) {
|
||||
form.startTime = undefined;
|
||||
form.endTime = undefined;
|
||||
form.expireDay = 30;
|
||||
} else {
|
||||
form.expireDay = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
/* 适用范围改变 */
|
||||
const onApplyRangeChange = (e: any) => {
|
||||
const value = e.target.value;
|
||||
applyCateListValue.value = [];
|
||||
applyItemListValue.value = [];
|
||||
form.applyRangeConfig = '';
|
||||
};
|
||||
|
||||
/* 搜索商品 */
|
||||
const searchGoods = async (value: string) => {
|
||||
if (value && value.trim()) {
|
||||
try {
|
||||
const res = await listShopGoods({ keywords: value.trim() });
|
||||
goodsList.value = res || [];
|
||||
console.log('搜索到的商品:', goodsList.value);
|
||||
} catch (e) {
|
||||
console.error('搜索商品失败:', e);
|
||||
goodsList.value = [];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/* 获取优惠券类型颜色 */
|
||||
const getCouponTypeColor = () => {
|
||||
const colorMap = {
|
||||
10: 'red',
|
||||
20: 'orange',
|
||||
30: 'green'
|
||||
};
|
||||
return colorMap[form.type] || 'blue';
|
||||
};
|
||||
|
||||
/* 获取优惠券类型名称 */
|
||||
const getCouponTypeName = () => {
|
||||
const nameMap = {
|
||||
10: '满减券',
|
||||
20: '折扣券',
|
||||
30: '免费券'
|
||||
};
|
||||
return nameMap[form.type] || '优惠券';
|
||||
};
|
||||
|
||||
/* 获取优惠券价值文本 */
|
||||
const getCouponValueText = () => {
|
||||
switch (form.type) {
|
||||
case 10:
|
||||
return `¥${form.reducePrice || 0}`;
|
||||
case 20:
|
||||
return `${form.discount || 0}折`;
|
||||
case 30:
|
||||
return '免费';
|
||||
default:
|
||||
return '优惠';
|
||||
}
|
||||
};
|
||||
|
||||
/* 获取有效期文本 */
|
||||
const getExpireText = () => {
|
||||
if (form.expireType === 10) {
|
||||
return `领取后${form.expireDay || 0}天内有效`;
|
||||
} else {
|
||||
const start = form.startTime
|
||||
? dayjs(form.startTime).format('YYYY.MM.DD')
|
||||
: '';
|
||||
const end = form.endTime ? dayjs(form.endTime).format('YYYY.MM.DD') : '';
|
||||
return start && end ? `${start} - ${end}` : '请设置有效期';
|
||||
}
|
||||
};
|
||||
|
||||
const { resetFields } = useForm(form, rules);
|
||||
|
||||
/* 保存编辑 */
|
||||
const save = () => {
|
||||
if (!formRef.value) {
|
||||
return;
|
||||
}
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(() => {
|
||||
loading.value = true;
|
||||
const formData = {
|
||||
...form
|
||||
};
|
||||
|
||||
// 处理时间字段转换
|
||||
if (formData.startTime && dayjs.isDayjs(formData.startTime)) {
|
||||
formData.startTime = formData.startTime.format('YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
if (formData.endTime && dayjs.isDayjs(formData.endTime)) {
|
||||
formData.endTime = formData.endTime.format('YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
|
||||
// 处理适用范围配置
|
||||
if (form.applyRange === 20 && applyItemListValue.value.length) {
|
||||
formData.couponApplyItemList = applyItemListValue.value.map((pk) => ({
|
||||
pk,
|
||||
type: 0
|
||||
}));
|
||||
} else {
|
||||
formData.couponApplyItemList = [];
|
||||
}
|
||||
|
||||
if (form.applyRange === 30 && applyCateListValue.value.length) {
|
||||
formData.couponApplyCateList = applyCateListValue.value.map(
|
||||
(cateId) => ({
|
||||
cateId,
|
||||
cateLevel: 0
|
||||
})
|
||||
);
|
||||
} else {
|
||||
formData.couponApplyCateList = [];
|
||||
}
|
||||
|
||||
const saveOrUpdate = isUpdate.value ? updateShopCoupon : addShopCoupon;
|
||||
saveOrUpdate(formData)
|
||||
.then((msg) => {
|
||||
loading.value = false;
|
||||
message.success(msg);
|
||||
updateVisible(false);
|
||||
emit('done');
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
message.error(e.message);
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
/* 获取商品列表 */
|
||||
const getGoodsList = async () => {
|
||||
try {
|
||||
const res = await listShopGoods({ pageSize: 50 });
|
||||
goodsList.value = res || [];
|
||||
console.log('获取到的商品列表:', goodsList.value);
|
||||
} catch (e) {
|
||||
console.error('获取商品列表失败:', e);
|
||||
goodsList.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
/* 获取商品分类列表 */
|
||||
const getGoodsCateList = async () => {
|
||||
try {
|
||||
const res = await listShopGoodsCategory();
|
||||
goodsCateList.value = res || [];
|
||||
console.log('获取到的商品分类列表:', goodsCateList.value);
|
||||
} catch (e) {
|
||||
console.error('获取商品分类列表失败:', e);
|
||||
goodsCateList.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
async (visible) => {
|
||||
if (visible) {
|
||||
await getGoodsList();
|
||||
await getGoodsCateList();
|
||||
|
||||
if (props.data) {
|
||||
assignObject(form, props.data);
|
||||
|
||||
// 处理时间字段转换
|
||||
if (props.data.startTime) {
|
||||
form.startTime = dayjs(props.data.startTime);
|
||||
}
|
||||
if (props.data.endTime) {
|
||||
form.endTime = dayjs(props.data.endTime);
|
||||
}
|
||||
|
||||
// 处理适用范围数据
|
||||
if (
|
||||
props.data.couponApplyCateList &&
|
||||
props.data.couponApplyCateList.length > 0
|
||||
) {
|
||||
applyCateListValue.value = props.data.couponApplyCateList.map(
|
||||
(item) => item.cateId
|
||||
);
|
||||
}
|
||||
if (
|
||||
props.data.couponApplyItemList &&
|
||||
props.data.couponApplyItemList.length > 0
|
||||
) {
|
||||
applyItemListValue.value = props.data.couponApplyItemList.map(
|
||||
(item) => item.pk
|
||||
);
|
||||
}
|
||||
|
||||
isUpdate.value = true;
|
||||
} else {
|
||||
// 重置为默认值
|
||||
Object.assign(form, {
|
||||
id: undefined,
|
||||
name: '',
|
||||
description: '',
|
||||
type: 10,
|
||||
reducePrice: undefined,
|
||||
discount: undefined,
|
||||
minPrice: 0,
|
||||
expireType: 10,
|
||||
expireDay: 30,
|
||||
startTime: undefined,
|
||||
endTime: undefined,
|
||||
applyRange: 10,
|
||||
applyRangeConfig: '',
|
||||
isExpire: 0,
|
||||
sortNumber: 100,
|
||||
status: 0,
|
||||
deleted: 0,
|
||||
userId: undefined,
|
||||
tenantId: undefined,
|
||||
createTime: undefined,
|
||||
updateTime: undefined,
|
||||
totalCount: -1,
|
||||
issuedCount: 0,
|
||||
limitPerUser: 1,
|
||||
enabled: 1
|
||||
});
|
||||
|
||||
isUpdate.value = false;
|
||||
}
|
||||
} else {
|
||||
applyCateListValue.value = [];
|
||||
applyItemListValue.value = [];
|
||||
resetFields();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.coupon-type-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.ant-tag {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
span {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.fixed-time-config,
|
||||
.apply-range-config {
|
||||
background: #fafafa;
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.coupon-preview {
|
||||
margin-top: 24px;
|
||||
|
||||
.coupon-card {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
color: white;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 50%;
|
||||
transform: translate(30px, -30px);
|
||||
}
|
||||
|
||||
.coupon-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.coupon-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.coupon-body {
|
||||
.coupon-name {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.coupon-desc {
|
||||
font-size: 14px;
|
||||
opacity: 0.9;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.coupon-condition {
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.coupon-footer {
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.2);
|
||||
|
||||
.coupon-expire {
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-divider-horizontal.ant-divider-with-text-left) {
|
||||
margin: 24px 0 16px 0;
|
||||
|
||||
.ant-divider-inner-text {
|
||||
padding: 0 16px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-form-item) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
:deep(.ant-radio) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.ant-radio-inner {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-select-selection-item) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:deep(.ant-input-number) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.ant-alert) {
|
||||
.ant-alert-message {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
744
src/views/shop/shopCoupon/index.vue
Normal file
744
src/views/shop/shopCoupon/index.vue
Normal file
@@ -0,0 +1,744 @@
|
||||
<template>
|
||||
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="openEdit()">
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
新增优惠券
|
||||
</a-button>
|
||||
<a-button @click="reload()">
|
||||
<template #icon>
|
||||
<ReloadOutlined />
|
||||
</template>
|
||||
刷新
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<a-card :bordered="false" :body-style="{ padding: '16px' }">
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-container">
|
||||
<a-form layout="inline" :model="searchForm" class="search-form">
|
||||
<a-form-item label="优惠券名称">
|
||||
<a-input
|
||||
v-model:value="searchForm.name"
|
||||
placeholder="请输入优惠券名称"
|
||||
allow-clear
|
||||
style="width: 200px"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="优惠券类型">
|
||||
<a-select
|
||||
v-model:value="searchForm.type"
|
||||
placeholder="请选择类型"
|
||||
allow-clear
|
||||
style="width: 150px"
|
||||
>
|
||||
<a-select-option :value="10">满减券</a-select-option>
|
||||
<a-select-option :value="20">折扣券</a-select-option>
|
||||
<a-select-option :value="30">免费券</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="到期类型">
|
||||
<a-select
|
||||
v-model:value="searchForm.expireType"
|
||||
placeholder="请选择到期类型"
|
||||
allow-clear
|
||||
style="width: 150px"
|
||||
>
|
||||
<a-select-option :value="10">领取后生效</a-select-option>
|
||||
<a-select-option :value="20">固定时间</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="是否过期">
|
||||
<a-select
|
||||
v-model:value="searchForm.isExpire"
|
||||
placeholder="请选择状态"
|
||||
allow-clear
|
||||
style="width: 120px"
|
||||
>
|
||||
<a-select-option :value="0">未过期</a-select-option>
|
||||
<a-select-option :value="1">已过期</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleSearch">
|
||||
<template #icon>
|
||||
<SearchOutlined />
|
||||
</template>
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button @click="handleReset">
|
||||
<template #icon>
|
||||
<ClearOutlined />
|
||||
</template>
|
||||
重置
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<!-- 批量操作区域 -->
|
||||
<div v-if="selection.length > 0" class="batch-actions">
|
||||
<a-alert
|
||||
:message="`已选择 ${selection.length} 项`"
|
||||
type="info"
|
||||
show-icon
|
||||
style="margin-bottom: 16px"
|
||||
>
|
||||
<template #action>
|
||||
<a-space>
|
||||
<a-button size="small" @click="clearSelection">取消选择</a-button>
|
||||
<a-popconfirm
|
||||
title="确定要删除选中的优惠券吗?"
|
||||
@confirm="removeBatch"
|
||||
>
|
||||
<a-button size="small" danger>批量删除</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-alert>
|
||||
</div>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ele-pro-table
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:columns="columns"
|
||||
:datasource="datasource"
|
||||
:customRow="customRow"
|
||||
:row-selection="rowSelection"
|
||||
:scroll="{ x: 1800 }"
|
||||
tool-class="ele-toolbar-form"
|
||||
class="coupon-table"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'name'">
|
||||
<div class="coupon-name">
|
||||
<a-typography-text strong>{{ record.name }}</a-typography-text>
|
||||
<div class="coupon-description">{{
|
||||
record.description || '暂无描述'
|
||||
}}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'type'">
|
||||
<a-tag :color="getCouponTypeColor(record.type)">
|
||||
{{ getCouponTypeText(record.type) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'value'">
|
||||
<div class="coupon-value">
|
||||
<template v-if="record.type === 10">
|
||||
<span class="value-amount"
|
||||
>¥{{ Number(record.reducePrice).toFixed(2) || '0.00' }}</span
|
||||
>
|
||||
<div class="value-condition"
|
||||
>满¥{{
|
||||
Number(record.minPrice).toFixed(2) || '0.00'
|
||||
}}可用</div
|
||||
>
|
||||
</template>
|
||||
<template v-else-if="record.type === 20">
|
||||
<span class="value-discount">{{ record.discount }}折</span>
|
||||
<div class="value-condition"
|
||||
>满¥{{
|
||||
Number(record.minPrice)?.toFixed(2) || '0.00'
|
||||
}}可用</div
|
||||
>
|
||||
</template>
|
||||
<template v-else-if="record.type === 30">
|
||||
<span class="value-free">免费券</span>
|
||||
<div class="value-condition"
|
||||
>满¥{{
|
||||
Number(record.minPrice)?.toFixed(2) || '0.00'
|
||||
}}可用</div
|
||||
>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'expireInfo'">
|
||||
<div class="expire-info">
|
||||
<a-tag :color="record.expireType === 10 ? 'blue' : 'green'">
|
||||
{{ record.expireType === 10 ? '领取后生效' : '固定时间' }}
|
||||
</a-tag>
|
||||
<div class="expire-detail">
|
||||
<template v-if="record.expireType === 10">
|
||||
{{ record.expireDay }}天有效
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ formatDate(record.startTime) }} 至
|
||||
{{ formatDate(record.endTime) }}
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'applyRange'">
|
||||
<a-tag :color="getApplyRangeColor(record.applyRange)">
|
||||
{{ getApplyRangeText(record.applyRange) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'usage'">
|
||||
<div class="usage-info">
|
||||
<a-progress
|
||||
:percent="getUsagePercent(record)"
|
||||
:stroke-color="getUsageColor(record)"
|
||||
size="small"
|
||||
/>
|
||||
<div class="usage-text">
|
||||
已发放: {{ record.issuedCount || 0 }}
|
||||
<template v-if="record.totalCount !== -1">
|
||||
/ {{ record.totalCount }}
|
||||
</template>
|
||||
<template v-else> (无限制) </template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'isExpire'">
|
||||
<a-tag :color="record.isExpire === 0 ? 'success' : 'error'">
|
||||
{{ record.isExpire === 0 ? '未过期' : '已过期' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-tooltip title="编辑">
|
||||
<a-button type="link" size="small" @click="openEdit(record)">
|
||||
<template #icon>
|
||||
<EditOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip title="复制">
|
||||
<a-button type="link" size="small" @click="copyRecord(record)">
|
||||
<template #icon>
|
||||
<CopyOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-popconfirm
|
||||
title="确定要删除此优惠券吗?"
|
||||
@confirm="remove(record)"
|
||||
>
|
||||
<a-tooltip title="删除">
|
||||
<a-button type="link" size="small" danger>
|
||||
<template #icon>
|
||||
<DeleteOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<ShopCouponEdit v-model:visible="showEdit" :data="current" @done="reload" />
|
||||
</a-page-header>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { createVNode, ref, reactive, computed } from 'vue';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import {
|
||||
ExclamationCircleOutlined,
|
||||
PlusOutlined,
|
||||
ReloadOutlined,
|
||||
SearchOutlined,
|
||||
ClearOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
CopyOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
import type { EleProTable } from 'ele-admin-pro';
|
||||
import { toDateString } from 'ele-admin-pro';
|
||||
import type {
|
||||
DatasourceFunction,
|
||||
ColumnItem
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import { getPageTitle } from '@/utils/common';
|
||||
import ShopCouponEdit from './components/shopCouponEdit.vue';
|
||||
import {
|
||||
pageShopCoupon,
|
||||
removeShopCoupon,
|
||||
removeBatchShopCoupon
|
||||
} from '@/api/shop/shopCoupon';
|
||||
import type {
|
||||
ShopCoupon,
|
||||
ShopCouponParam
|
||||
} from '@/api/shop/shopCoupon/model';
|
||||
|
||||
// 表格实例
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
|
||||
// 表格选中数据
|
||||
const selection = ref<ShopCoupon[]>([]);
|
||||
// 当前编辑数据
|
||||
const current = ref<ShopCoupon | null>(null);
|
||||
// 是否显示编辑弹窗
|
||||
const showEdit = ref(false);
|
||||
// 加载状态
|
||||
const loading = ref(false);
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive<ShopCouponParam>({
|
||||
keywords: '',
|
||||
name: undefined,
|
||||
type: undefined
|
||||
});
|
||||
|
||||
// 表格数据源
|
||||
const datasource: DatasourceFunction = ({
|
||||
page,
|
||||
limit,
|
||||
where,
|
||||
orders,
|
||||
filters
|
||||
}) => {
|
||||
const params = {
|
||||
...where,
|
||||
...searchForm,
|
||||
...orders,
|
||||
page,
|
||||
limit
|
||||
};
|
||||
|
||||
if (filters) {
|
||||
Object.assign(params, filters);
|
||||
}
|
||||
|
||||
return pageShopCoupon(params);
|
||||
};
|
||||
|
||||
// 行选择配置
|
||||
const rowSelection = computed(() => ({
|
||||
selectedRowKeys: selection.value.map((item) => item.id),
|
||||
onChange: (
|
||||
selectedRowKeys: (string | number)[],
|
||||
selectedRows: ShopCoupon[]
|
||||
) => {
|
||||
selection.value = selectedRows;
|
||||
},
|
||||
onSelect: (record: ShopCoupon, selected: boolean) => {
|
||||
if (selected) {
|
||||
selection.value.push(record);
|
||||
} else {
|
||||
const index = selection.value.findIndex(
|
||||
(item) => item.id === record.id
|
||||
);
|
||||
if (index > -1) {
|
||||
selection.value.splice(index, 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
onSelectAll: (
|
||||
selected: boolean,
|
||||
selectedRows: ShopCoupon[],
|
||||
changeRows: ShopCoupon[]
|
||||
) => {
|
||||
if (selected) {
|
||||
changeRows.forEach((row) => {
|
||||
if (!selection.value.find((item) => item.id === row.id)) {
|
||||
selection.value.push(row);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
changeRows.forEach((row) => {
|
||||
const index = selection.value.findIndex((item) => item.id === row.id);
|
||||
if (index > -1) {
|
||||
selection.value.splice(index, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// 表格列配置
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
align: 'center',
|
||||
width: 80,
|
||||
fixed: 'left'
|
||||
},
|
||||
{
|
||||
title: '优惠券信息',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
align: 'left',
|
||||
width: 250,
|
||||
fixed: 'left',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
align: 'center',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '优惠价值',
|
||||
dataIndex: 'value',
|
||||
key: 'value',
|
||||
align: 'center',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '有效期信息',
|
||||
dataIndex: 'expireInfo',
|
||||
key: 'expireInfo',
|
||||
align: 'center',
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
title: '适用范围',
|
||||
dataIndex: 'applyRange',
|
||||
key: 'applyRange',
|
||||
align: 'center',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '使用情况',
|
||||
dataIndex: 'usage',
|
||||
key: 'usage',
|
||||
align: 'center',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '每人限领',
|
||||
dataIndex: 'limitPerUser',
|
||||
key: 'limitPerUser',
|
||||
align: 'center',
|
||||
width: 100,
|
||||
customRender: ({ text }) => (text === -1 ? '无限制' : text)
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'isExpire',
|
||||
key: 'isExpire',
|
||||
align: 'center',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
align: 'center',
|
||||
width: 120,
|
||||
sorter: true,
|
||||
ellipsis: true,
|
||||
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd')
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 150,
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
hideInSetting: true
|
||||
}
|
||||
]);
|
||||
|
||||
// 工具方法
|
||||
const getCouponTypeText = (type: number) => {
|
||||
const typeMap = {
|
||||
10: '满减券',
|
||||
20: '折扣券',
|
||||
30: '免费券'
|
||||
};
|
||||
return typeMap[type as keyof typeof typeMap] || '未知';
|
||||
};
|
||||
|
||||
const getCouponTypeColor = (type: number) => {
|
||||
const colorMap = {
|
||||
10: 'red',
|
||||
20: 'orange',
|
||||
30: 'green'
|
||||
};
|
||||
return colorMap[type as keyof typeof colorMap] || 'default';
|
||||
};
|
||||
|
||||
const getApplyRangeText = (range: number) => {
|
||||
const rangeMap = {
|
||||
10: '全部商品',
|
||||
20: '指定商品',
|
||||
30: '指定分类'
|
||||
};
|
||||
return rangeMap[range as keyof typeof rangeMap] || '未知';
|
||||
};
|
||||
|
||||
const getApplyRangeColor = (range: number) => {
|
||||
const colorMap = {
|
||||
10: 'blue',
|
||||
20: 'purple',
|
||||
30: 'cyan'
|
||||
};
|
||||
return colorMap[range as keyof typeof colorMap] || 'default';
|
||||
};
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
return dateStr ? toDateString(dateStr, 'yyyy-MM-dd') : '-';
|
||||
};
|
||||
|
||||
const getUsagePercent = (record: ShopCoupon) => {
|
||||
if (record.totalCount === -1) return 0;
|
||||
return Math.round(
|
||||
((record.issuedCount || 0) / Number(record.totalCount)) * 100
|
||||
);
|
||||
};
|
||||
|
||||
const getUsageColor = (record: ShopCoupon) => {
|
||||
const percent = getUsagePercent(record);
|
||||
if (percent >= 90) return '#ff4d4f';
|
||||
if (percent >= 70) return '#faad14';
|
||||
return '#52c41a';
|
||||
};
|
||||
|
||||
/* 搜索 */
|
||||
const reload = (where?: ShopCouponParam) => {
|
||||
selection.value = [];
|
||||
tableRef?.value?.reload({ where: where });
|
||||
};
|
||||
|
||||
/* 处理搜索 */
|
||||
const handleSearch = () => {
|
||||
reload();
|
||||
};
|
||||
|
||||
/* 重置搜索 */
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, {
|
||||
name: '',
|
||||
type: undefined,
|
||||
expireType: undefined,
|
||||
isExpire: undefined
|
||||
});
|
||||
reload();
|
||||
};
|
||||
|
||||
/* 清除选择 */
|
||||
const clearSelection = () => {
|
||||
selection.value = [];
|
||||
};
|
||||
|
||||
/* 打开编辑弹窗 */
|
||||
const openEdit = (row?: ShopCoupon) => {
|
||||
current.value = row ?? null;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
/* 复制记录 */
|
||||
const copyRecord = (record: ShopCoupon) => {
|
||||
const copyData = {
|
||||
...record,
|
||||
id: undefined,
|
||||
name: `${record.name}_副本`,
|
||||
createTime: undefined,
|
||||
updateTime: undefined,
|
||||
issuedCount: 0
|
||||
};
|
||||
current.value = copyData;
|
||||
showEdit.value = true;
|
||||
message.success('已复制优惠券信息,请修改后保存');
|
||||
};
|
||||
|
||||
/* 删除单个 */
|
||||
const remove = (row: ShopCoupon) => {
|
||||
if (row.issuedCount && row.issuedCount > 0) {
|
||||
message.warning('该优惠券已有用户领取,无法删除');
|
||||
return;
|
||||
}
|
||||
|
||||
const hide = message.loading('删除中...', 0);
|
||||
removeShopCoupon(row.id)
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
/* 批量删除 */
|
||||
const removeBatch = () => {
|
||||
if (!selection.value.length) {
|
||||
message.error('请至少选择一条数据');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否有已发放的优惠券
|
||||
const issuedCoupons = selection.value.filter(
|
||||
(item) => item.issuedCount && item.issuedCount > 0
|
||||
);
|
||||
if (issuedCoupons.length > 0) {
|
||||
message.warning(
|
||||
`选中的优惠券中有 ${issuedCoupons.length} 个已被用户领取,无法删除`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Modal.confirm({
|
||||
title: '批量删除确认',
|
||||
content: `确定要删除选中的 ${selection.value.length} 个优惠券吗?此操作不可恢复。`,
|
||||
icon: createVNode(ExclamationCircleOutlined),
|
||||
maskClosable: true,
|
||||
okText: '确定删除',
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
const hide = message.loading('批量删除中...', 0);
|
||||
removeBatchShopCoupon(selection.value.map((d) => d.id))
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
selection.value = [];
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/* 自定义行属性 */
|
||||
const customRow = (record: ShopCoupon) => {
|
||||
return {
|
||||
// 行点击事件
|
||||
onClick: () => {
|
||||
// console.log(record);
|
||||
},
|
||||
// 行双击事件
|
||||
onDblclick: () => {
|
||||
openEdit(record);
|
||||
},
|
||||
// 行样式
|
||||
class: record.isExpire === 1 ? 'expired-row' : ''
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ShopCoupon'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.shop-coupon-container {
|
||||
.search-container {
|
||||
background: #fafafa;
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.search-form {
|
||||
.ant-form-item {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.batch-actions {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.coupon-table {
|
||||
.coupon-name {
|
||||
text-align: left;
|
||||
|
||||
.coupon-description {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.coupon-value {
|
||||
.value-amount {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #f5222d;
|
||||
}
|
||||
|
||||
.value-discount {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #fa8c16;
|
||||
}
|
||||
|
||||
.value-free {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.value-condition {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.expire-info {
|
||||
.expire-detail {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.usage-info {
|
||||
.usage-text {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.expired-row {
|
||||
background-color: #fff2f0;
|
||||
|
||||
td {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-table) {
|
||||
.ant-table-tbody > tr:hover > td {
|
||||
background-color: #e6f7ff;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-progress) {
|
||||
.ant-progress-text {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-alert) {
|
||||
.ant-alert-message {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
158
src/views/shop/shopDealerApply/components/search.vue
Normal file
158
src/views/shop/shopDealerApply/components/search.vue
Normal 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>
|
||||
@@ -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>
|
||||
458
src/views/shop/shopDealerApply/index.vue
Normal file
458
src/views/shop/shopDealerApply/index.vue
Normal 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>
|
||||
42
src/views/shop/shopDealerCapital/components/search.vue
Normal file
42
src/views/shop/shopDealerCapital/components/search.vue
Normal 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>
|
||||
@@ -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>
|
||||
306
src/views/shop/shopDealerCapital/index.vue
Normal file
306
src/views/shop/shopDealerCapital/index.vue
Normal file
@@ -0,0 +1,306 @@
|
||||
<template>
|
||||
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
|
||||
<a-card :bordered="false" :body-style="{ padding: '16px' }">
|
||||
<ele-pro-table
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:columns="columns"
|
||||
:datasource="datasource"
|
||||
:customRow="customRow"
|
||||
tool-class="ele-toolbar-form"
|
||||
class="sys-org-table"
|
||||
>
|
||||
<template #toolbar>
|
||||
<search
|
||||
@search="reload"
|
||||
:selection="selection"
|
||||
@add="openEdit"
|
||||
@remove="removeBatch"
|
||||
@batchMove="openMove"
|
||||
/>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'image'">
|
||||
<a-image :src="record.image" :width="50" />
|
||||
</template>
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag v-if="record.status === 0" color="green">显示</a-tag>
|
||||
<a-tag v-if="record.status === 1" color="red">隐藏</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a @click="openEdit(record)">修改</a>
|
||||
<a-divider type="vertical" />
|
||||
<a-popconfirm
|
||||
title="确定要删除此记录吗?"
|
||||
@confirm="remove(record)"
|
||||
>
|
||||
<a class="ele-text-danger">删除</a>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<ShopDealerCapitalEdit
|
||||
v-model:visible="showEdit"
|
||||
:data="current"
|
||||
@done="reload"
|
||||
/>
|
||||
</a-page-header>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { createVNode, ref } from 'vue';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
|
||||
import type { EleProTable } from 'ele-admin-pro';
|
||||
import { toDateString } from 'ele-admin-pro';
|
||||
import type {
|
||||
DatasourceFunction,
|
||||
ColumnItem
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import Search from './components/search.vue';
|
||||
import { getPageTitle } from '@/utils/common';
|
||||
import ShopDealerCapitalEdit from './components/shopDealerCapitalEdit.vue';
|
||||
import {
|
||||
pageShopDealerCapital,
|
||||
removeShopDealerCapital,
|
||||
removeBatchShopDealerCapital
|
||||
} from '@/api/shop/shopDealerCapital';
|
||||
import type {
|
||||
ShopDealerCapital,
|
||||
ShopDealerCapitalParam
|
||||
} from '@/api/shop/shopDealerCapital/model';
|
||||
|
||||
// 表格实例
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
|
||||
// 表格选中数据
|
||||
const selection = ref<ShopDealerCapital[]>([]);
|
||||
// 当前编辑数据
|
||||
const current = ref<ShopDealerCapital | null>(null);
|
||||
// 是否显示编辑弹窗
|
||||
const showEdit = ref(false);
|
||||
// 是否显示批量移动弹窗
|
||||
const showMove = ref(false);
|
||||
// 加载状态
|
||||
const loading = ref(true);
|
||||
|
||||
// 表格数据源
|
||||
const datasource: DatasourceFunction = ({
|
||||
page,
|
||||
limit,
|
||||
where,
|
||||
orders,
|
||||
filters
|
||||
}) => {
|
||||
if (filters) {
|
||||
where.status = filters.status;
|
||||
}
|
||||
return pageShopDealerCapital({
|
||||
...where,
|
||||
...orders,
|
||||
page,
|
||||
limit
|
||||
});
|
||||
};
|
||||
|
||||
// 表格列配置
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
align: 'center',
|
||||
width: 80,
|
||||
fixed: 'left'
|
||||
},
|
||||
{
|
||||
title: '用户ID',
|
||||
dataIndex: 'userId',
|
||||
key: 'userId',
|
||||
align: 'center',
|
||||
width: 100,
|
||||
fixed: 'left'
|
||||
},
|
||||
{
|
||||
title: '流动类型',
|
||||
dataIndex: 'flowType',
|
||||
key: 'flowType',
|
||||
align: 'center',
|
||||
width: 120,
|
||||
customRender: ({ text }) => {
|
||||
const typeMap = {
|
||||
10: { text: '佣金收入', color: 'success' },
|
||||
20: { text: '提现支出', color: 'warning' },
|
||||
30: { text: '转账支出', color: 'error' },
|
||||
40: { text: '转账收入', color: 'processing' }
|
||||
};
|
||||
const type = typeMap[text] || { text: '未知', color: 'default' };
|
||||
return {
|
||||
type: 'tag',
|
||||
props: { color: type.color },
|
||||
children: type.text
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '金额',
|
||||
dataIndex: 'money',
|
||||
key: 'money',
|
||||
align: 'center',
|
||||
width: 120,
|
||||
customRender: ({ text, record }) => {
|
||||
const amount = parseFloat(text || '0').toFixed(2);
|
||||
const isIncome = record.flowType === 10 || record.flowType === 40;
|
||||
return {
|
||||
type: 'span',
|
||||
props: {
|
||||
style: {
|
||||
color: isIncome ? '#52c41a' : '#ff4d4f',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
children: `${isIncome ? '+' : '-'}¥${amount}`
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '关联订单',
|
||||
dataIndex: 'orderNo',
|
||||
key: 'orderNo',
|
||||
align: 'center',
|
||||
customRender: ({ text }) => text || '-'
|
||||
},
|
||||
{
|
||||
title: '对方用户',
|
||||
dataIndex: 'toUserId',
|
||||
key: 'toUserId',
|
||||
align: 'center',
|
||||
width: 100,
|
||||
customRender: ({ text }) => (text ? `ID: ${text}` : '-')
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
dataIndex: 'describe',
|
||||
key: 'describe',
|
||||
align: 'left',
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
customRender: ({ text }) => text || '-'
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
align: 'center',
|
||||
sorter: true,
|
||||
ellipsis: true,
|
||||
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd')
|
||||
},
|
||||
{
|
||||
title: '修改时间',
|
||||
dataIndex: 'updateTime',
|
||||
key: 'updateTime',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 180,
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
hideInSetting: true
|
||||
}
|
||||
]);
|
||||
|
||||
/* 搜索 */
|
||||
const reload = (where?: ShopDealerCapitalParam) => {
|
||||
selection.value = [];
|
||||
tableRef?.value?.reload({ where: where });
|
||||
};
|
||||
|
||||
/* 打开编辑弹窗 */
|
||||
const openEdit = (row?: ShopDealerCapital) => {
|
||||
current.value = row ?? null;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
/* 打开批量移动弹窗 */
|
||||
const openMove = () => {
|
||||
showMove.value = true;
|
||||
};
|
||||
|
||||
/* 删除单个 */
|
||||
const remove = (row: ShopDealerCapital) => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeShopDealerCapital(row.id)
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
/* 批量删除 */
|
||||
const removeBatch = () => {
|
||||
if (!selection.value.length) {
|
||||
message.error('请至少选择一条数据');
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确定要删除选中的记录吗?',
|
||||
icon: createVNode(ExclamationCircleOutlined),
|
||||
maskClosable: true,
|
||||
onOk: () => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeBatchShopDealerCapital(selection.value.map((d) => d.id))
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/* 查询 */
|
||||
const query = () => {
|
||||
loading.value = true;
|
||||
};
|
||||
|
||||
/* 自定义行属性 */
|
||||
const customRow = (record: ShopDealerCapital) => {
|
||||
return {
|
||||
// 行点击事件
|
||||
onClick: () => {
|
||||
// console.log(record);
|
||||
},
|
||||
// 行双击事件
|
||||
onDblclick: () => {
|
||||
openEdit(record);
|
||||
}
|
||||
};
|
||||
};
|
||||
query();
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ShopDealerCapital'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
89
src/views/shop/shopDealerOrder/components/Import.vue
Normal file
89
src/views/shop/shopDealerOrder/components/Import.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<!-- 经销商订单导入弹窗 -->
|
||||
<template>
|
||||
<ele-modal
|
||||
:width="520"
|
||||
:footer="null"
|
||||
title="导入分销订单"
|
||||
:visible="visible"
|
||||
@update:visible="updateVisible"
|
||||
>
|
||||
<a-spin :spinning="loading">
|
||||
<a-upload-dragger
|
||||
accept=".xls,.xlsx"
|
||||
:show-upload-list="false"
|
||||
:customRequest="doUpload"
|
||||
style="padding: 24px 0; margin-bottom: 16px"
|
||||
>
|
||||
<p class="ant-upload-drag-icon">
|
||||
<cloud-upload-outlined />
|
||||
</p>
|
||||
<p class="ant-upload-hint">将文件拖到此处,或点击上传</p>
|
||||
</a-upload-dragger>
|
||||
<div class="ant-upload-text text-gray-400">
|
||||
<div
|
||||
>1、必须按<a
|
||||
href="https://oss.wsdns.cn/20251018/408b805ec3cd4084a4dc686e130af578.xlsx"
|
||||
target="_blank"
|
||||
>导入模版</a
|
||||
>的格式上传</div
|
||||
>
|
||||
<div>2、导入成功确认结算完成佣金的发放</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { message } from 'ant-design-vue/es';
|
||||
import { CloudUploadOutlined } from '@ant-design/icons-vue';
|
||||
import { importSdyDealerOrder } from '@/api/sdy/sdyDealerOrder';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
defineProps<{
|
||||
// 是否打开弹窗
|
||||
visible: boolean;
|
||||
}>();
|
||||
|
||||
// 导入请求状态
|
||||
const loading = ref(false);
|
||||
|
||||
/* 上传 */
|
||||
const doUpload = ({ file }) => {
|
||||
if (
|
||||
![
|
||||
'application/vnd.ms-excel',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
].includes(file.type)
|
||||
) {
|
||||
message.error('只能选择 excel 文件');
|
||||
return false;
|
||||
}
|
||||
if (file.size / 1024 / 1024 > 10) {
|
||||
message.error('大小不能超过 10MB');
|
||||
return false;
|
||||
}
|
||||
loading.value = true;
|
||||
importSdyDealerOrder(file)
|
||||
.then((msg) => {
|
||||
loading.value = false;
|
||||
message.success(msg);
|
||||
updateVisible(false);
|
||||
emit('done');
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
message.error(e.message);
|
||||
});
|
||||
return false;
|
||||
};
|
||||
|
||||
/* 更新 visible */
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
</script>
|
||||
182
src/views/shop/shopDealerOrder/components/search.vue
Normal file
182
src/views/shop/shopDealerOrder/components/search.vue
Normal file
@@ -0,0 +1,182 @@
|
||||
<template>
|
||||
<div class="flex items-center gap-20">
|
||||
<!-- 搜索表单 -->
|
||||
<a-form
|
||||
:model="where"
|
||||
layout="inline"
|
||||
class="search-form"
|
||||
@finish="handleSearch"
|
||||
>
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button
|
||||
danger
|
||||
class="ele-btn-icon"
|
||||
v-if="selection.length > 0"
|
||||
:disabled="selection?.length === 0"
|
||||
@click="removeBatch"
|
||||
>
|
||||
<template #icon>
|
||||
<DeleteOutlined />
|
||||
</template>
|
||||
<span>批量删除</span>
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
|
||||
<!-- <a-form-item label="订单状态">-->
|
||||
<!-- <a-select-->
|
||||
<!-- v-model:value="where.isInvalid"-->
|
||||
<!-- placeholder="全部"-->
|
||||
<!-- allow-clear-->
|
||||
<!-- style="width: 120px"-->
|
||||
<!-- >-->
|
||||
<!-- <a-select-option :value="0">有效</a-select-option>-->
|
||||
<!-- <a-select-option :value="1">失效</a-select-option>-->
|
||||
<!-- </a-select>-->
|
||||
<!-- </a-form-item>-->
|
||||
|
||||
<!-- <a-form-item label="结算状态">-->
|
||||
<!-- <a-select-->
|
||||
<!-- v-model:value="where.isSettled"-->
|
||||
<!-- placeholder="全部"-->
|
||||
<!-- allow-clear-->
|
||||
<!-- style="width: 120px"-->
|
||||
<!-- >-->
|
||||
<!-- <a-select-option :value="0">未结算</a-select-option>-->
|
||||
<!-- <a-select-option :value="1">已结算</a-select-option>-->
|
||||
<!-- </a-select>-->
|
||||
<!-- </a-form-item>-->
|
||||
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-input-search
|
||||
allow-clear
|
||||
placeholder="请输入关键词"
|
||||
style="width: 240px"
|
||||
v-model:value="where.keywords"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
<!-- <a-button type="primary" html-type="submit" class="ele-btn-icon">-->
|
||||
<!-- <template #icon>-->
|
||||
<!-- <SearchOutlined/>-->
|
||||
<!-- </template>-->
|
||||
<!-- 搜索-->
|
||||
<!-- </a-button>-->
|
||||
<a-button @click="resetSearch"> 重置 </a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-divider type="vertical" />
|
||||
<a-space>
|
||||
<!-- <a-button @click="exportData" class="ele-btn-icon">-->
|
||||
<!-- <template #icon>-->
|
||||
<!-- <ExportOutlined />-->
|
||||
<!-- </template>-->
|
||||
<!-- 导出数据-->
|
||||
<!-- </a-button>-->
|
||||
<a-button @click="openImport" class="ele-btn-icon">
|
||||
<template #icon>
|
||||
<UploadOutlined />
|
||||
</template>
|
||||
导入数据
|
||||
</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
danger
|
||||
@click="batchSettle"
|
||||
:disabled="selection?.length === 0"
|
||||
>
|
||||
<template #icon>
|
||||
<DollarOutlined />
|
||||
</template>
|
||||
批量结算
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<!-- 导入弹窗 -->
|
||||
<Import v-model:visible="showImport" @done="emit('importDone')" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import {
|
||||
DollarOutlined,
|
||||
UploadOutlined,
|
||||
DeleteOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
import type { ShopDealerOrderParam } from '@/api/shop/shopDealerOrder/model';
|
||||
import Import from './Import.vue';
|
||||
import useSearch from '@/utils/use-search';
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
// 选中的数据
|
||||
selection?: any[];
|
||||
}>(),
|
||||
{
|
||||
selection: () => []
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'search', where?: ShopDealerOrderParam): void;
|
||||
(e: 'batchSettle'): void;
|
||||
(e: 'export'): void;
|
||||
(e: 'importDone'): void;
|
||||
(e: 'remove'): void;
|
||||
}>();
|
||||
|
||||
// 是否显示导入弹窗
|
||||
const showImport = ref(false);
|
||||
|
||||
// 搜索表单
|
||||
const { where, resetFields } = useSearch<ShopDealerOrderParam>({
|
||||
orderNo: '',
|
||||
productName: '',
|
||||
isInvalid: undefined,
|
||||
isSettled: undefined
|
||||
});
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
const searchParams = { ...where };
|
||||
// 清除空值
|
||||
Object.keys(searchParams).forEach((key) => {
|
||||
if (searchParams[key] === '' || searchParams[key] === undefined) {
|
||||
delete searchParams[key];
|
||||
}
|
||||
});
|
||||
emit('search', searchParams);
|
||||
};
|
||||
|
||||
// 重置搜索
|
||||
const resetSearch = () => {
|
||||
// Object.keys(searchForm).forEach(key => {
|
||||
// searchForm[key] = key === 'orderId' ? undefined : '';
|
||||
// });
|
||||
resetFields();
|
||||
emit('search', {});
|
||||
};
|
||||
|
||||
// 批量删除
|
||||
const removeBatch = () => {
|
||||
emit('remove');
|
||||
};
|
||||
|
||||
// 批量结算
|
||||
const batchSettle = () => {
|
||||
emit('batchSettle');
|
||||
};
|
||||
|
||||
// 导出数据
|
||||
const exportData = () => {
|
||||
emit('export');
|
||||
};
|
||||
|
||||
// 打开导入弹窗
|
||||
const openImport = () => {
|
||||
showImport.value = true;
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,363 @@
|
||||
<!-- 编辑弹窗 -->
|
||||
<template>
|
||||
<ele-modal
|
||||
:width="900"
|
||||
:visible="visible"
|
||||
:maskClosable="false"
|
||||
:maxable="maxable"
|
||||
:title="isUpdate ? '分销订单' : '分销订单'"
|
||||
:body-style="{ paddingBottom: '28px' }"
|
||||
@update:visible="updateVisible"
|
||||
:okText="`立即结算`"
|
||||
@ok="save"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
>
|
||||
<!-- 订单基本信息 -->
|
||||
<a-divider orientation="left">
|
||||
<span style="color: #1890ff; font-weight: 600">基本信息</span>
|
||||
</a-divider>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="客户名称" name="title">
|
||||
{{ form.title }}
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="订单编号" name="orderNo">
|
||||
{{ form.orderNo }}
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="结算电量" name="orderPrice">
|
||||
{{ parseFloat(form.orderPrice || 0).toFixed(2) }}
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="换算成度" name="dealerPrice">
|
||||
{{ parseFloat(form.degreePrice || 0).toFixed(2) }}
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="税率" name="rate">
|
||||
{{ form.rate }}
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="单价" name="price">
|
||||
{{ form.price }}
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="结算金额" name="payPrice">
|
||||
{{ parseFloat(form.settledPrice || 0).toFixed(2) }}
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="实发金额" name="payPrice">
|
||||
{{ parseFloat(form.payPrice || 0).toFixed(2) }}
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<div class="font-bold text-gray-400 bg-gray-50">开发调试</div>
|
||||
<div class="text-gray-400 bg-gray-50">
|
||||
<div>业务员({{ form.userId }}):{{ form.nickname }}</div>
|
||||
<div
|
||||
>一级分销商({{ form.firstUserId }}):{{
|
||||
form.firstNickname
|
||||
}},一级佣金30%:{{ form.firstMoney }}</div
|
||||
>
|
||||
<div
|
||||
>二级分销商({{ form.secondUserId }}):{{
|
||||
form.secondNickname
|
||||
}},二级佣金10%:{{ form.secondMoney }}</div
|
||||
>
|
||||
<div
|
||||
>三级分销商({{ form.thirdUserId }}):{{
|
||||
form.thirdNickname
|
||||
}},三级佣金60%:{{ form.thirdMoney }}</div
|
||||
>
|
||||
</div>
|
||||
<!-- 分销商信息 -->
|
||||
<a-divider orientation="left">
|
||||
<span style="color: #1890ff; font-weight: 600">收益计算</span>
|
||||
</a-divider>
|
||||
|
||||
<!-- 一级分销商 -->
|
||||
<div class="dealer-section">
|
||||
<h4 class="dealer-title">
|
||||
<a-tag color="orange">一级佣金30%</a-tag>
|
||||
</h4>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="用户ID" name="firstUserId">
|
||||
{{ form.firstUserId }}
|
||||
</a-form-item>
|
||||
<a-form-item label="昵称" name="firstNickname">
|
||||
{{ form.firstNickname }}
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="占比" name="rate">
|
||||
{{ '30%' }}
|
||||
</a-form-item>
|
||||
<a-form-item label="获取收益" name="firstMoney">
|
||||
{{ form.firstMoney }}
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<!-- 二级分销商 -->
|
||||
<div class="dealer-section">
|
||||
<h4 class="dealer-title">
|
||||
<a-tag color="orange">二级佣金10%</a-tag>
|
||||
</h4>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="用户ID" name="secondUserId">
|
||||
{{ form.secondUserId }}
|
||||
</a-form-item>
|
||||
<a-form-item label="昵称" name="nickname">
|
||||
{{ form.secondNickname }}
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="占比" name="rate"> 10% </a-form-item>
|
||||
<a-form-item label="获取收益" name="firstMoney">
|
||||
{{ form.secondMoney }}
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<!-- 三级分销商 -->
|
||||
<div class="dealer-section" v-if="form.thirdUserId > 0">
|
||||
<h4 class="dealer-title">
|
||||
<a-tag color="orange">三级佣金60%</a-tag>
|
||||
</h4>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="用户ID" name="thirdUserId">
|
||||
{{ form.thirdUserId }}
|
||||
</a-form-item>
|
||||
<a-form-item label="昵称" name="thirdNickname">
|
||||
{{ form.thirdNickname }}
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="占比" name="rate">
|
||||
{{ '60%' }}
|
||||
</a-form-item>
|
||||
<a-form-item label="获取收益" name="thirdMoney">
|
||||
{{ form.thirdMoney }}
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<a-form-item
|
||||
label="结算时间"
|
||||
name="settleTime"
|
||||
v-if="form.isSettled === 1"
|
||||
>
|
||||
{{ form.settleTime }}
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { Form, message } from 'ant-design-vue';
|
||||
import { assignObject } from 'ele-admin-pro';
|
||||
import { ShopDealerOrder } from '@/api/shop/shopDealerOrder/model';
|
||||
import { FormInstance } from 'ant-design-vue/es/form';
|
||||
import { updateSdyDealerOrder } from '@/api/sdy/sdyDealerOrder';
|
||||
|
||||
// 是否是修改
|
||||
const isUpdate = ref(false);
|
||||
const useForm = Form.useForm;
|
||||
|
||||
const props = defineProps<{
|
||||
// 弹窗是否打开
|
||||
visible: boolean;
|
||||
// 修改回显的数据
|
||||
data?: ShopDealerOrder | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
// 提交状态
|
||||
const loading = ref(false);
|
||||
// 是否显示最大化切换按钮
|
||||
const maxable = ref(true);
|
||||
// 表格选中数据
|
||||
const formRef = ref<FormInstance | null>(null);
|
||||
|
||||
// 表单数据
|
||||
const form = reactive<ShopDealerOrder>({
|
||||
id: undefined,
|
||||
userId: undefined,
|
||||
nickname: undefined,
|
||||
orderNo: undefined,
|
||||
title: undefined,
|
||||
orderPrice: undefined,
|
||||
settledPrice: undefined,
|
||||
degreePrice: undefined,
|
||||
price: undefined,
|
||||
month: undefined,
|
||||
payPrice: undefined,
|
||||
firstUserId: undefined,
|
||||
secondUserId: undefined,
|
||||
thirdUserId: undefined,
|
||||
firstMoney: undefined,
|
||||
secondMoney: undefined,
|
||||
thirdMoney: undefined,
|
||||
firstNickname: undefined,
|
||||
secondNickname: undefined,
|
||||
thirdNickname: undefined,
|
||||
rate: undefined,
|
||||
comments: undefined,
|
||||
isInvalid: 0,
|
||||
isSettled: 0,
|
||||
settleTime: undefined,
|
||||
tenantId: undefined,
|
||||
createTime: undefined,
|
||||
updateTime: undefined
|
||||
});
|
||||
|
||||
/* 更新visible */
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive({
|
||||
userId: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择用户ID',
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const { resetFields } = useForm(form, rules);
|
||||
|
||||
/* 保存编辑 */
|
||||
const save = () => {
|
||||
if (!formRef.value) {
|
||||
return;
|
||||
}
|
||||
if (form.isSettled == 1) {
|
||||
message.error('请勿重复结算');
|
||||
return;
|
||||
}
|
||||
if (form.userId == 0) {
|
||||
message.error('未签约');
|
||||
return;
|
||||
}
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(() => {
|
||||
loading.value = true;
|
||||
const formData = {
|
||||
...form,
|
||||
isSettled: 1
|
||||
};
|
||||
updateSdyDealerOrder(formData)
|
||||
.then((msg) => {
|
||||
loading.value = false;
|
||||
message.success(msg);
|
||||
updateVisible(false);
|
||||
emit('done');
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
message.error(e.message);
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
console.log(localStorage.getItem(''));
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
if (props.data) {
|
||||
assignObject(form, props.data);
|
||||
isUpdate.value = true;
|
||||
} else {
|
||||
// 重置为默认值
|
||||
Object.assign(form, {
|
||||
id: undefined,
|
||||
userId: undefined,
|
||||
orderNo: undefined,
|
||||
orderPrice: undefined,
|
||||
firstUserId: undefined,
|
||||
secondUserId: undefined,
|
||||
thirdUserId: undefined,
|
||||
firstMoney: undefined,
|
||||
secondMoney: undefined,
|
||||
thirdMoney: undefined,
|
||||
isInvalid: 0,
|
||||
isSettled: 0,
|
||||
settleTime: undefined
|
||||
});
|
||||
isUpdate.value = false;
|
||||
}
|
||||
} else {
|
||||
resetFields();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.dealer-section {
|
||||
margin-bottom: 24px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid #1890ff;
|
||||
|
||||
.dealer-title {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
|
||||
.ant-tag {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-divider-horizontal.ant-divider-with-text-left) {
|
||||
margin: 24px 0 16px 0;
|
||||
|
||||
.ant-divider-inner-text {
|
||||
padding: 0 16px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-form-item) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
:deep(.ant-input-number) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
448
src/views/shop/shopDealerOrder/index.vue
Normal file
448
src/views/shop/shopDealerOrder/index.vue
Normal file
@@ -0,0 +1,448 @@
|
||||
<template>
|
||||
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
|
||||
<a-card :bordered="false" :body-style="{ padding: '16px' }">
|
||||
<ele-pro-table
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:columns="columns"
|
||||
:datasource="datasource"
|
||||
:customRow="customRow"
|
||||
v-model:selection="selection"
|
||||
tool-class="ele-toolbar-form"
|
||||
class="sys-org-table"
|
||||
>
|
||||
<template #toolbar>
|
||||
<search
|
||||
@search="reload"
|
||||
:selection="selection"
|
||||
@batchSettle="batchSettle"
|
||||
@export="handleExport"
|
||||
@remove="removeBatch"
|
||||
@importDone="reload"
|
||||
/>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'title'">
|
||||
<div>{{ record.title }}</div>
|
||||
<div class="text-gray-400">用户ID:{{ record.userId }}</div>
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'orderPrice'">
|
||||
{{ record.orderPrice.toFixed(2) }}
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'degreePrice'">
|
||||
{{ record.degreePrice.toFixed(2) }}
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'price'">
|
||||
{{ record.price }}
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'settledPrice'">
|
||||
{{ record.settledPrice.toFixed(2) }}
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'payPrice'">
|
||||
{{ record.payPrice.toFixed(2) }}
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'dealerInfo'">
|
||||
<div class="dealer-info">
|
||||
<div v-if="record.firstUserId" class="dealer-level">
|
||||
<a-tag color="red">一级</a-tag>
|
||||
用户{{ record.firstUserId }} - ¥{{
|
||||
parseFloat(record.firstMoney || '0').toFixed(2)
|
||||
}}
|
||||
</div>
|
||||
<div v-if="record.secondUserId" class="dealer-level">
|
||||
<a-tag color="orange">二级</a-tag>
|
||||
用户{{ record.secondUserId }} - ¥{{
|
||||
parseFloat(record.secondMoney || '0').toFixed(2)
|
||||
}}
|
||||
</div>
|
||||
<div v-if="record.thirdUserId" class="dealer-level">
|
||||
<a-tag color="gold">三级</a-tag>
|
||||
用户{{ record.thirdUserId }} - ¥{{
|
||||
parseFloat(record.thirdMoney || '0').toFixed(2)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'isInvalid'">
|
||||
<a-tag v-if="record.isInvalid === 0" color="success">已签约</a-tag>
|
||||
<a-tag v-if="record.isInvalid === 1" color="error">未签约</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'isSettled'">
|
||||
<a-tag v-if="record.isSettled === 0" color="orange">未结算</a-tag>
|
||||
<a-tag v-if="record.isSettled === 1" color="success">已结算</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'createTime'">
|
||||
<div class="flex flex-col">
|
||||
<a-tooltip title="创建时间">
|
||||
<span class="text-gray-500">{{ record.createTime }}</span>
|
||||
</a-tooltip>
|
||||
<a-tooltip title="结算时间">
|
||||
<span class="text-purple-500">{{ record.settleTime }}</span>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'action'">
|
||||
<template v-if="record.isSettled === 0 && record.isInvalid === 0">
|
||||
<a @click="openEdit(record)" class="ele-text-success"> 结算 </a>
|
||||
<a-divider type="vertical" />
|
||||
</template>
|
||||
<a-popconfirm
|
||||
v-if="record.isSettled === 0"
|
||||
title="确定要删除吗?"
|
||||
@confirm="remove(record)"
|
||||
placement="topRight"
|
||||
>
|
||||
<a class="text-red-500"> 删除 </a>
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<ShopDealerOrderEdit
|
||||
v-model:visible="showEdit"
|
||||
:data="current"
|
||||
@done="reload"
|
||||
/>
|
||||
</a-page-header>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { createVNode, ref } from 'vue';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
|
||||
import type { EleProTable } from 'ele-admin-pro';
|
||||
import type {
|
||||
DatasourceFunction,
|
||||
ColumnItem
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import Search from './components/search.vue';
|
||||
import { getPageTitle } from '@/utils/common';
|
||||
import ShopDealerOrderEdit from './components/shopDealerOrderEdit.vue';
|
||||
import {
|
||||
pageShopDealerOrder,
|
||||
removeShopDealerOrder,
|
||||
removeBatchShopDealerOrder
|
||||
} from '@/api/shop/shopDealerOrder';
|
||||
import type {
|
||||
ShopDealerOrder,
|
||||
ShopDealerOrderParam
|
||||
} from '@/api/shop/shopDealerOrder/model';
|
||||
import { exportSdyDealerOrder } from '@/api/sdy/sdyDealerOrder';
|
||||
|
||||
// 表格实例
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
|
||||
// 表格选中数据
|
||||
const selection = ref<ShopDealerOrder[]>([]);
|
||||
// 当前编辑数据
|
||||
const current = ref<ShopDealerOrder | null>(null);
|
||||
// 是否显示编辑弹窗
|
||||
const showEdit = ref(false);
|
||||
// 加载状态
|
||||
const loading = ref(true);
|
||||
|
||||
// 当前搜索条件
|
||||
const currentWhere = ref<ShopDealerOrderParam>({});
|
||||
|
||||
// 表格数据源
|
||||
const datasource: DatasourceFunction = ({
|
||||
page,
|
||||
limit,
|
||||
where,
|
||||
orders,
|
||||
filters
|
||||
}) => {
|
||||
if (filters) {
|
||||
where.status = filters.status;
|
||||
}
|
||||
// 保存当前搜索条件用于导出
|
||||
currentWhere.value = { ...where };
|
||||
// 未结算订单
|
||||
where.isSettled = 0;
|
||||
where.myOrder = 1;
|
||||
return pageShopDealerOrder({
|
||||
...where,
|
||||
...orders,
|
||||
page,
|
||||
limit
|
||||
});
|
||||
};
|
||||
|
||||
// 表格列配置
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
title: '订单编号',
|
||||
dataIndex: 'orderNo',
|
||||
key: 'orderNo'
|
||||
},
|
||||
{
|
||||
title: '客户名称',
|
||||
dataIndex: 'title',
|
||||
key: 'title',
|
||||
width: 220
|
||||
},
|
||||
{
|
||||
title: '结算电量',
|
||||
dataIndex: 'orderPrice',
|
||||
key: 'orderPrice',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '换算成度',
|
||||
dataIndex: 'degreePrice',
|
||||
key: 'degreePrice',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '结算单价',
|
||||
dataIndex: 'price',
|
||||
key: 'price',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '结算金额',
|
||||
dataIndex: 'settledPrice',
|
||||
key: 'settledPrice',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '税费',
|
||||
dataIndex: 'rate',
|
||||
key: 'rate',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '实发金额',
|
||||
dataIndex: 'payPrice',
|
||||
key: 'payPrice',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '签约状态',
|
||||
dataIndex: 'isInvalid',
|
||||
key: 'isInvalid',
|
||||
align: 'center',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '月份',
|
||||
dataIndex: 'month',
|
||||
key: 'month',
|
||||
align: 'center',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '结算状态',
|
||||
dataIndex: 'isSettled',
|
||||
key: 'isSettled',
|
||||
align: 'center',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 180,
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
hideInSetting: true
|
||||
}
|
||||
]);
|
||||
|
||||
/* 搜索 */
|
||||
const reload = (where?: ShopDealerOrderParam) => {
|
||||
selection.value = [];
|
||||
tableRef?.value?.reload({ where: where });
|
||||
};
|
||||
|
||||
/* 批量结算 */
|
||||
const batchSettle = () => {
|
||||
if (!selection.value.length) {
|
||||
message.error('请至少选择一条数据');
|
||||
return;
|
||||
}
|
||||
|
||||
const validOrders = selection.value.filter(
|
||||
(order) => order.isSettled === 0 && order.isInvalid === 0
|
||||
);
|
||||
|
||||
if (!validOrders.length) {
|
||||
message.error('所选订单中没有可结算的订单');
|
||||
return;
|
||||
}
|
||||
|
||||
const totalCommission = validOrders
|
||||
.reduce((sum, order) => {
|
||||
return (
|
||||
sum +
|
||||
parseFloat(order.firstMoney || '0') +
|
||||
parseFloat(order.secondMoney || '0') +
|
||||
parseFloat(order.thirdMoney || '0')
|
||||
);
|
||||
}, 0)
|
||||
.toFixed(2);
|
||||
|
||||
Modal.confirm({
|
||||
title: '批量结算确认',
|
||||
content: `确定要结算选中的 ${validOrders.length} 个订单吗?总佣金金额:¥${totalCommission}`,
|
||||
icon: createVNode(ExclamationCircleOutlined),
|
||||
okText: '确认结算',
|
||||
okType: 'primary',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
const hide = message.loading('正在批量结算...', 0);
|
||||
// 这里调用批量结算API
|
||||
setTimeout(() => {
|
||||
hide();
|
||||
message.success(`成功结算 ${validOrders.length} 个订单`);
|
||||
reload();
|
||||
}, 1500);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/* 导出数据 */
|
||||
const handleExport = () => {
|
||||
// 调用导出API,传入当前搜索条件
|
||||
exportSdyDealerOrder(currentWhere.value);
|
||||
};
|
||||
|
||||
/* 打开编辑弹窗 */
|
||||
const openEdit = (row?: ShopDealerOrder) => {
|
||||
current.value = row ?? null;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
/* 删除单个 */
|
||||
const remove = (row: ShopDealerOrder) => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeShopDealerOrder(row.id)
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
/* 批量删除 */
|
||||
const removeBatch = () => {
|
||||
if (!selection.value.length) {
|
||||
message.error('请至少选择一条数据');
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确定要删除选中的记录吗?',
|
||||
icon: createVNode(ExclamationCircleOutlined),
|
||||
maskClosable: true,
|
||||
onOk: () => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeBatchShopDealerOrder(selection.value.map((d) => d.id))
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/* 查询 */
|
||||
const query = () => {
|
||||
loading.value = true;
|
||||
};
|
||||
|
||||
/* 自定义行属性 */
|
||||
const customRow = (record: ShopDealerOrder) => {
|
||||
return {
|
||||
// 行点击事件
|
||||
onClick: () => {
|
||||
// console.log(record);
|
||||
},
|
||||
// 行双击事件
|
||||
onDblclick: () => {
|
||||
openEdit(record);
|
||||
}
|
||||
};
|
||||
};
|
||||
query();
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ShopDealerOrder'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.order-info {
|
||||
.order-id {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.order-price {
|
||||
color: #ff4d4f;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.dealer-info {
|
||||
.dealer-level {
|
||||
margin-bottom: 6px;
|
||||
font-size: 12px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.detail-section) {
|
||||
h4 {
|
||||
color: #1890ff;
|
||||
margin-bottom: 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 4px 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-table-tbody > tr > td) {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
:deep(.ant-tag) {
|
||||
margin: 2px 4px 2px 0;
|
||||
}
|
||||
</style>
|
||||
779
src/views/shop/shopDealerPoster/index.vue
Normal file
779
src/views/shop/shopDealerPoster/index.vue
Normal 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>
|
||||
204
src/views/shop/shopDealerReferee/components/RefereeTree.vue
Normal file
204
src/views/shop/shopDealerReferee/components/RefereeTree.vue
Normal 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>
|
||||
185
src/views/shop/shopDealerReferee/components/search.vue
Normal file
185
src/views/shop/shopDealerReferee/components/search.vue
Normal 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>
|
||||
@@ -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>
|
||||
563
src/views/shop/shopDealerReferee/index.vue
Normal file
563
src/views/shop/shopDealerReferee/index.vue
Normal 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>
|
||||
42
src/views/shop/shopDealerSetting/components/search.vue
Normal file
42
src/views/shop/shopDealerSetting/components/search.vue
Normal 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>
|
||||
@@ -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>
|
||||
415
src/views/shop/shopDealerSetting/index.vue
Normal file
415
src/views/shop/shopDealerSetting/index.vue
Normal file
@@ -0,0 +1,415 @@
|
||||
<template>
|
||||
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
|
||||
<a-card :bordered="false" :body-style="{ padding: '24px' }">
|
||||
<!-- 设置标签页 -->
|
||||
<a-tabs v-model:activeKey="activeTab" type="card" class="setting-tabs">
|
||||
<a-tab-pane key="basic" tab="基础设置">
|
||||
<a-form
|
||||
:model="basicSettings"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
layout="horizontal"
|
||||
>
|
||||
<!-- 是否开启分销功能 -->
|
||||
<a-form-item label="是否开启分销功能">
|
||||
<a-radio-group v-model:value="basicSettings.enableDistribution">
|
||||
<a-radio :value="true">开启</a-radio>
|
||||
<a-radio :value="false">关闭</a-radio>
|
||||
</a-radio-group>
|
||||
<div class="setting-desc">开启后用户可以申请成为分销商</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 分销层级 -->
|
||||
<a-form-item label="分销层级">
|
||||
<a-radio-group v-model:value="basicSettings.distributionLevel">
|
||||
<a-radio :value="1">一级</a-radio>
|
||||
<a-radio :value="2">二级</a-radio>
|
||||
<a-radio :value="3">三级</a-radio>
|
||||
</a-radio-group>
|
||||
<div class="setting-desc">设置分销商推荐层级关系</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 分销商内购 -->
|
||||
<a-form-item label="分销商内购">
|
||||
<a-radio-group v-model:value="basicSettings.dealerSelfBuy">
|
||||
<a-radio :value="true">开启</a-radio>
|
||||
<a-radio :value="false">关闭</a-radio>
|
||||
</a-radio-group>
|
||||
<div class="setting-desc"
|
||||
>分销商自己购买是否获得佣金,开启一般佣金</div
|
||||
>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="commission" tab="分销条件">
|
||||
<a-form
|
||||
:model="commissionSettings"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
layout="horizontal"
|
||||
>
|
||||
<!-- 申请方式 -->
|
||||
<a-form-item label="申请方式">
|
||||
<a-radio-group v-model:value="commissionSettings.applyType">
|
||||
<a-radio :value="10">需后台审核</a-radio>
|
||||
<a-radio :value="20">无需审核</a-radio>
|
||||
</a-radio-group>
|
||||
<div class="setting-desc">设置用户申请分销商的审核方式</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 佣金结算 -->
|
||||
<a-form-item label="佣金结算">
|
||||
<a-radio-group v-model:value="commissionSettings.settlementType">
|
||||
<a-radio :value="10">订单完成</a-radio>
|
||||
<a-radio :value="20">订单确认收货</a-radio>
|
||||
</a-radio-group>
|
||||
<div class="setting-desc">设置佣金何时结算到分销商账户</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 最低提现金额 -->
|
||||
<a-form-item label="最低提现金额">
|
||||
<a-input-number
|
||||
v-model:value="commissionSettings.minWithdrawAmount"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
style="width: 200px"
|
||||
>
|
||||
<template #addonAfter>元</template>
|
||||
</a-input-number>
|
||||
<div class="setting-desc">分销商申请提现的最低金额限制</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="withdraw" tab="提现设置">
|
||||
<a-form
|
||||
:model="withdrawSettings"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
layout="horizontal"
|
||||
>
|
||||
<!-- 提现方式 -->
|
||||
<a-form-item label="提现方式">
|
||||
<a-checkbox-group
|
||||
v-model:value="withdrawSettings.withdrawMethods"
|
||||
>
|
||||
<a-checkbox :value="10">微信</a-checkbox>
|
||||
<a-checkbox :value="20">支付宝</a-checkbox>
|
||||
<a-checkbox :value="30">银行卡</a-checkbox>
|
||||
</a-checkbox-group>
|
||||
<div class="setting-desc">设置支持的提现方式</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 提现手续费 -->
|
||||
<a-form-item label="提现手续费">
|
||||
<a-input-number
|
||||
v-model:value="withdrawSettings.withdrawFeeRate"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:precision="2"
|
||||
style="width: 200px"
|
||||
>
|
||||
<template #addonAfter>%</template>
|
||||
</a-input-number>
|
||||
<div class="setting-desc">提现时收取的手续费比例</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 提现审核 -->
|
||||
<a-form-item label="提现审核">
|
||||
<a-radio-group v-model:value="withdrawSettings.withdrawAudit">
|
||||
<a-radio :value="true">需要审核</a-radio>
|
||||
<a-radio :value="false">无需审核</a-radio>
|
||||
</a-radio-group>
|
||||
<div class="setting-desc">设置提现申请是否需要人工审核</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="agreement" tab="协议">
|
||||
<a-form
|
||||
:model="agreementSettings"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
layout="horizontal"
|
||||
>
|
||||
<!-- 分销商协议 -->
|
||||
<a-form-item label="分销商协议">
|
||||
<a-textarea
|
||||
v-model:value="agreementSettings.dealerAgreement"
|
||||
:rows="10"
|
||||
placeholder="请输入分销商协议内容..."
|
||||
/>
|
||||
<div class="setting-desc">用户申请分销商时需要同意的协议内容</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="notification" tab="自定义文字">
|
||||
<a-form
|
||||
:model="notificationSettings"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
layout="horizontal"
|
||||
>
|
||||
<!-- 申请成功提示 -->
|
||||
<a-form-item label="申请成功提示">
|
||||
<a-textarea
|
||||
v-model:value="notificationSettings.applySuccessText"
|
||||
:rows="3"
|
||||
placeholder="请输入申请成功后的提示文字..."
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 申请失败提示 -->
|
||||
<a-form-item label="申请失败提示">
|
||||
<a-textarea
|
||||
v-model:value="notificationSettings.applyFailText"
|
||||
:rows="3"
|
||||
placeholder="请输入申请失败后的提示文字..."
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 提现成功提示 -->
|
||||
<a-form-item label="提现成功提示">
|
||||
<a-textarea
|
||||
v-model:value="notificationSettings.withdrawSuccessText"
|
||||
:rows="3"
|
||||
placeholder="请输入提现成功后的提示文字..."
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="page" tab="页面设置">
|
||||
<a-form
|
||||
:model="pageSettings"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
layout="horizontal"
|
||||
>
|
||||
<!-- 分销中心标题 -->
|
||||
<a-form-item label="分销中心标题">
|
||||
<a-input
|
||||
v-model:value="pageSettings.centerTitle"
|
||||
placeholder="请输入分销中心页面标题"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 分销中心背景图 -->
|
||||
<a-form-item label="分销中心背景图">
|
||||
<a-upload
|
||||
v-model:file-list="pageSettings.backgroundImages"
|
||||
list-type="picture-card"
|
||||
:max-count="1"
|
||||
@preview="handlePreview"
|
||||
>
|
||||
<div v-if="pageSettings.backgroundImages.length < 1">
|
||||
<PlusOutlined />
|
||||
<div style="margin-top: 8px">上传</div>
|
||||
</div>
|
||||
</a-upload>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
|
||||
<!-- 保存按钮 -->
|
||||
<div class="setting-footer">
|
||||
<a-button
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="saveSettings"
|
||||
:loading="saving"
|
||||
>
|
||||
保存设置
|
||||
</a-button>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-page-header>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||
import { getPageTitle } from '@/utils/common';
|
||||
import {
|
||||
updateShopDealerSetting,
|
||||
getShopDealerSetting
|
||||
} from '@/api/shop/shopDealerSetting';
|
||||
|
||||
// 当前激活的标签页
|
||||
const activeTab = ref('basic');
|
||||
// 保存状态
|
||||
const saving = ref(false);
|
||||
|
||||
// 基础设置
|
||||
const basicSettings = reactive({
|
||||
enableDistribution: true,
|
||||
distributionLevel: 3,
|
||||
dealerSelfBuy: false
|
||||
});
|
||||
|
||||
// 分销条件设置
|
||||
const commissionSettings = reactive({
|
||||
applyType: 10,
|
||||
settlementType: 10,
|
||||
minWithdrawAmount: 100
|
||||
});
|
||||
|
||||
// 提现设置
|
||||
const withdrawSettings = reactive({
|
||||
withdrawMethods: [10, 20, 30],
|
||||
withdrawFeeRate: 0,
|
||||
withdrawAudit: true
|
||||
});
|
||||
|
||||
// 协议设置
|
||||
const agreementSettings = reactive({
|
||||
dealerAgreement: '分销商协议内容...'
|
||||
});
|
||||
|
||||
// 通知设置
|
||||
const notificationSettings = reactive({
|
||||
applySuccessText: '恭喜您成功成为分销商!',
|
||||
applyFailText: '很抱歉,您的申请未通过审核。',
|
||||
withdrawSuccessText: '提现申请已提交,请耐心等待处理。'
|
||||
});
|
||||
|
||||
// 页面设置
|
||||
const pageSettings = reactive({
|
||||
centerTitle: '分销中心',
|
||||
backgroundImages: []
|
||||
});
|
||||
|
||||
/* 图片预览 */
|
||||
const handlePreview = (file: any) => {
|
||||
console.log('预览图片:', file);
|
||||
};
|
||||
|
||||
/* 加载设置 */
|
||||
const loadSettings = async () => {
|
||||
try {
|
||||
// 这里应该调用API获取设置数据
|
||||
// const settings = await getShopDealerSetting();
|
||||
// 然后将数据分配到各个设置对象中
|
||||
console.log('加载设置数据');
|
||||
} catch (error) {
|
||||
console.error('加载设置失败:', error);
|
||||
message.error('加载设置失败');
|
||||
}
|
||||
};
|
||||
|
||||
/* 保存设置 */
|
||||
const saveSettings = async () => {
|
||||
saving.value = true;
|
||||
try {
|
||||
// 收集所有设置数据
|
||||
const allSettings = {
|
||||
basic: basicSettings,
|
||||
commission: commissionSettings,
|
||||
withdraw: withdrawSettings,
|
||||
agreement: agreementSettings,
|
||||
notification: notificationSettings,
|
||||
page: pageSettings
|
||||
};
|
||||
|
||||
console.log('保存设置:', allSettings);
|
||||
|
||||
// 这里应该调用API保存设置
|
||||
// await updateShopDealerSetting(allSettings);
|
||||
|
||||
// 模拟保存
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
message.success('设置保存成功');
|
||||
} catch (error) {
|
||||
console.error('保存设置失败:', error);
|
||||
message.error('保存设置失败');
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 页面加载时获取设置数据
|
||||
onMounted(() => {
|
||||
loadSettings();
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ShopDealerSetting'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.dealer-setting-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.setting-tabs {
|
||||
:deep(.ant-tabs-card > .ant-tabs-nav .ant-tabs-tab) {
|
||||
border-radius: 6px 6px 0 0;
|
||||
background: #fafafa;
|
||||
border: 1px solid #d9d9d9;
|
||||
margin-right: 8px;
|
||||
|
||||
&.ant-tabs-tab-active {
|
||||
background: #fff;
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-tabs-content-holder) {
|
||||
background: #fff;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 0 6px 6px 6px;
|
||||
padding: 24px;
|
||||
min-height: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-desc {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.setting-footer {
|
||||
text-align: center;
|
||||
margin-top: 32px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
:deep(.ant-form-item-label > label) {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.ant-radio-group) {
|
||||
.ant-radio-wrapper {
|
||||
margin-right: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-checkbox-group) {
|
||||
.ant-checkbox-wrapper {
|
||||
margin-right: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-upload-select-picture-card) {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
:deep(.ant-upload-list-picture-card .ant-upload-list-item) {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
</style>
|
||||
89
src/views/shop/shopDealerWithdraw/components/Import.vue
Normal file
89
src/views/shop/shopDealerWithdraw/components/Import.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<!-- 经销商订单导入弹窗 -->
|
||||
<template>
|
||||
<ele-modal
|
||||
:width="520"
|
||||
:footer="null"
|
||||
title="导入分销订单"
|
||||
:visible="visible"
|
||||
@update:visible="updateVisible"
|
||||
>
|
||||
<a-spin :spinning="loading">
|
||||
<a-upload-dragger
|
||||
accept=".xls,.xlsx"
|
||||
:show-upload-list="false"
|
||||
:customRequest="doUpload"
|
||||
style="padding: 24px 0; margin-bottom: 16px"
|
||||
>
|
||||
<p class="ant-upload-drag-icon">
|
||||
<cloud-upload-outlined />
|
||||
</p>
|
||||
<p class="ant-upload-hint">将文件拖到此处,或点击上传</p>
|
||||
</a-upload-dragger>
|
||||
<div class="ant-upload-text text-gray-400">
|
||||
<div
|
||||
>1、必须按<a
|
||||
href="https://oss.wsdns.cn/20251018/408b805ec3cd4084a4dc686e130af578.xlsx"
|
||||
target="_blank"
|
||||
>导入模版</a
|
||||
>的格式上传</div
|
||||
>
|
||||
<div>2、导入成功确认结算完成佣金的发放</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { message } from 'ant-design-vue/es';
|
||||
import { CloudUploadOutlined } from '@ant-design/icons-vue';
|
||||
import { importSdyDealerOrder } from '@/api/sdy/sdyDealerOrder';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
defineProps<{
|
||||
// 是否打开弹窗
|
||||
visible: boolean;
|
||||
}>();
|
||||
|
||||
// 导入请求状态
|
||||
const loading = ref(false);
|
||||
|
||||
/* 上传 */
|
||||
const doUpload = ({ file }) => {
|
||||
if (
|
||||
![
|
||||
'application/vnd.ms-excel',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
].includes(file.type)
|
||||
) {
|
||||
message.error('只能选择 excel 文件');
|
||||
return false;
|
||||
}
|
||||
if (file.size / 1024 / 1024 > 10) {
|
||||
message.error('大小不能超过 10MB');
|
||||
return false;
|
||||
}
|
||||
loading.value = true;
|
||||
importSdyDealerOrder(file)
|
||||
.then((msg) => {
|
||||
loading.value = false;
|
||||
message.success(msg);
|
||||
updateVisible(false);
|
||||
emit('done');
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
message.error(e.message);
|
||||
});
|
||||
return false;
|
||||
};
|
||||
|
||||
/* 更新 visible */
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
</script>
|
||||
101
src/views/shop/shopDealerWithdraw/components/search.vue
Normal file
101
src/views/shop/shopDealerWithdraw/components/search.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<div class="flex items-center gap-20">
|
||||
<!-- 搜索表单 -->
|
||||
<a-form
|
||||
:model="where"
|
||||
layout="inline"
|
||||
class="search-form"
|
||||
@finish="handleSearch"
|
||||
>
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button
|
||||
danger
|
||||
class="ele-btn-icon"
|
||||
v-if="selection.length > 0"
|
||||
:disabled="selection?.length === 0"
|
||||
@click="removeBatch"
|
||||
>
|
||||
<template #icon>
|
||||
<DeleteOutlined />
|
||||
</template>
|
||||
<span>批量删除</span>
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-input-search
|
||||
allow-clear
|
||||
placeholder="请输入用户ID"
|
||||
style="width: 240px"
|
||||
v-model:value="where.keywords"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
<a-button @click="resetSearch"> 重置 </a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<!-- 导入弹窗 -->
|
||||
<Import v-model:visible="showImport" @done="emit('importDone')" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { DeleteOutlined } from '@ant-design/icons-vue';
|
||||
import Import from './Import.vue';
|
||||
import useSearch from '@/utils/use-search';
|
||||
import { ShopDealerWithdrawParam } from '@/api/shop/shopDealerWithdraw/model';
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
// 选中的数据
|
||||
selection?: any[];
|
||||
}>(),
|
||||
{
|
||||
selection: () => []
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'search', where?: ShopDealerWithdrawParam): void;
|
||||
(e: 'batchSettle'): void;
|
||||
(e: 'export'): void;
|
||||
(e: 'importDone'): void;
|
||||
(e: 'remove'): void;
|
||||
}>();
|
||||
|
||||
// 是否显示导入弹窗
|
||||
const showImport = ref(false);
|
||||
|
||||
// 搜索表单
|
||||
const { where, resetFields } = useSearch<ShopDealerWithdrawParam>({
|
||||
keywords: ''
|
||||
});
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
const searchParams = { ...where };
|
||||
// 清除空值
|
||||
Object.keys(searchParams).forEach((key) => {
|
||||
if (searchParams[key] === '' || searchParams[key] === undefined) {
|
||||
delete searchParams[key];
|
||||
}
|
||||
});
|
||||
emit('search', searchParams);
|
||||
};
|
||||
|
||||
// 重置搜索
|
||||
const resetSearch = () => {
|
||||
resetFields();
|
||||
emit('search', {});
|
||||
};
|
||||
|
||||
// 批量删除
|
||||
const removeBatch = () => {
|
||||
emit('remove');
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,697 @@
|
||||
<!-- 编辑弹窗 -->
|
||||
<template>
|
||||
<ele-modal
|
||||
:width="1000"
|
||||
:visible="visible"
|
||||
:maskClosable="false"
|
||||
:maxable="maxable"
|
||||
:title="isUpdate ? '编辑提现申请' : '新增提现申请'"
|
||||
:body-style="{ paddingBottom: '28px' }"
|
||||
@update:visible="updateVisible"
|
||||
@ok="save"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
>
|
||||
<!-- 基本信息 -->
|
||||
<a-divider orientation="left">
|
||||
<span style="color: #1890ff; font-weight: 600">基本信息</span>
|
||||
</a-divider>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="备注" name="comments">
|
||||
<div class="text-red-500">{{ form.comments }}</div>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 收款信息 -->
|
||||
<a-divider orientation="left">
|
||||
<span style="color: #1890ff; font-weight: 600">收款信息</span>
|
||||
</a-divider>
|
||||
|
||||
<!-- 微信收款信息 -->
|
||||
<div v-if="form.payType === 10" class="payment-info wechat-info">
|
||||
<a-alert
|
||||
message="微信收款信息"
|
||||
description="请确保微信账号信息准确,以免影响到账"
|
||||
type="success"
|
||||
show-icon
|
||||
style="margin-bottom: 16px"
|
||||
/>
|
||||
<a-form-item label="微信号" name="wechatAccount">
|
||||
<a-input
|
||||
placeholder="请输入微信号"
|
||||
v-model:value="form.wechatAccount"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="微信昵称" name="wechatName">
|
||||
<a-input
|
||||
placeholder="请输入微信昵称"
|
||||
v-model:value="form.wechatName"
|
||||
/>
|
||||
</a-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 支付宝收款信息 -->
|
||||
<div v-if="form.payType === 20" class="payment-info alipay-info">
|
||||
<a-alert
|
||||
message="支付宝收款信息"
|
||||
description="请确保支付宝账号信息准确,姓名需与实名认证一致"
|
||||
type="info"
|
||||
show-icon
|
||||
style="margin-bottom: 16px"
|
||||
/>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="支付宝姓名" name="alipayName">
|
||||
<a-input
|
||||
placeholder="请输入支付宝实名姓名"
|
||||
disabled
|
||||
v-model:value="form.alipayName"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="支付宝账号" name="alipayAccount">
|
||||
<a-input
|
||||
placeholder="请输入支付宝账号"
|
||||
disabled
|
||||
v-model:value="form.alipayAccount"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<!-- 银行卡收款信息 -->
|
||||
<div v-if="form.payType === 30" class="payment-info bank-info">
|
||||
<a-alert
|
||||
message="银行卡收款信息"
|
||||
description="请确保银行卡信息准确,开户名需与身份证姓名一致"
|
||||
type="warning"
|
||||
show-icon
|
||||
style="margin-bottom: 16px"
|
||||
/>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="开户行名称" name="bankName">
|
||||
{{ form.bankName }}
|
||||
</a-form-item>
|
||||
<a-form-item label="银行开户名" name="bankAccount">
|
||||
{{ form.bankAccount }}
|
||||
</a-form-item>
|
||||
<a-form-item label="银行卡号" name="bankCard">
|
||||
{{ form.bankCard }}
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<!-- 审核信息 -->
|
||||
<a-divider orientation="left">
|
||||
<span style="color: #1890ff; font-weight: 600">审核信息</span>
|
||||
</a-divider>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="申请状态" name="applyStatus">
|
||||
<a-select
|
||||
v-model:value="form.applyStatus"
|
||||
:disabled="form.applyStatus == 40 || form.applyStatus == 30"
|
||||
placeholder="请选择申请状态"
|
||||
>
|
||||
<a-select-option :value="10">
|
||||
<div class="status-option">
|
||||
<a-tag color="orange">待审核</a-tag>
|
||||
<span>等待审核</span>
|
||||
</div>
|
||||
</a-select-option>
|
||||
<a-select-option :value="20">
|
||||
<div class="status-option">
|
||||
<a-tag color="success">审核通过</a-tag>
|
||||
<span>审核通过</span>
|
||||
</div>
|
||||
</a-select-option>
|
||||
<a-select-option :value="30">
|
||||
<div class="status-option">
|
||||
<a-tag color="error">审核驳回</a-tag>
|
||||
<span>审核驳回</span>
|
||||
</div>
|
||||
</a-select-option>
|
||||
<a-select-option :value="40">
|
||||
<div class="status-option">
|
||||
<a-tag>已打款</a-tag>
|
||||
<span>已完成打款</span>
|
||||
</div>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="驳回原因"
|
||||
name="rejectReason"
|
||||
v-if="form.applyStatus === 30"
|
||||
>
|
||||
<a-textarea
|
||||
v-model:value="form.rejectReason"
|
||||
placeholder="请输入驳回原因"
|
||||
:rows="3"
|
||||
:maxlength="200"
|
||||
show-count
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
label="上传支付凭证"
|
||||
name="image"
|
||||
v-if="form.applyStatus === 40"
|
||||
>
|
||||
<SelectFile
|
||||
:placeholder="`请选择图片`"
|
||||
:limit="2"
|
||||
:data="files"
|
||||
@done="chooseFile"
|
||||
@del="onDeleteFile"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 提现预览 -->
|
||||
<div class="withdraw-preview" v-if="form.money && form.payType">
|
||||
<a-alert
|
||||
:type="getPreviewAlertType()"
|
||||
:message="getPreviewText()"
|
||||
show-icon
|
||||
style="margin-top: 16px"
|
||||
/>
|
||||
</div>
|
||||
</a-form>
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { Form, message } from 'ant-design-vue';
|
||||
import { assignObject, uuid } from 'ele-admin-pro';
|
||||
import {
|
||||
addShopDealerWithdraw,
|
||||
updateShopDealerWithdraw
|
||||
} from '@/api/shop/shopDealerWithdraw';
|
||||
import { ShopDealerWithdraw } from '@/api/shop/shopDealerWithdraw/model';
|
||||
import { FormInstance } from 'ant-design-vue/es/form';
|
||||
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
|
||||
import dayjs from 'dayjs';
|
||||
import { FileRecord } from '@/api/system/file/model';
|
||||
|
||||
// 是否是修改
|
||||
const isUpdate = ref(false);
|
||||
const useForm = Form.useForm;
|
||||
|
||||
const props = defineProps<{
|
||||
// 弹窗是否打开
|
||||
visible: boolean;
|
||||
// 修改回显的数据
|
||||
data?: ShopDealerWithdraw | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
// 提交状态
|
||||
const loading = ref<boolean>(false);
|
||||
const isSuccess = ref<boolean>(false);
|
||||
// 是否显示最大化切换按钮
|
||||
const maxable = ref(true);
|
||||
const files = ref<ItemType[]>([]);
|
||||
// 表格选中数据
|
||||
const formRef = ref<FormInstance | null>(null);
|
||||
|
||||
// 表单数据
|
||||
const form = reactive<ShopDealerWithdraw>({
|
||||
id: undefined,
|
||||
userId: undefined,
|
||||
realName: undefined,
|
||||
nickname: undefined,
|
||||
phone: undefined,
|
||||
avatar: undefined,
|
||||
money: undefined,
|
||||
payType: undefined,
|
||||
// 微信相关
|
||||
wechatAccount: '',
|
||||
wechatName: '',
|
||||
// 支付宝相关
|
||||
alipayName: '',
|
||||
alipayAccount: '',
|
||||
// 银行卡相关
|
||||
bankName: '',
|
||||
bankAccount: '',
|
||||
bankCard: '',
|
||||
// 审核相关
|
||||
applyStatus: 10,
|
||||
auditTime: undefined,
|
||||
rejectReason: '',
|
||||
platform: '',
|
||||
comments: '',
|
||||
tenantId: undefined,
|
||||
createTime: undefined,
|
||||
updateTime: undefined
|
||||
});
|
||||
|
||||
/* 更新visible */
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive({
|
||||
userId: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入分销商用户ID',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
money: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入提现金额',
|
||||
trigger: 'blur'
|
||||
},
|
||||
{
|
||||
validator: (rule: any, value: any) => {
|
||||
if (value && value <= 0) {
|
||||
return Promise.reject('提现金额必须大于0');
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
payType: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择打款方式',
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
platform: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择来源平台',
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
// 微信验证
|
||||
wechatAccount: [
|
||||
{
|
||||
validator: (rule: any, value: any) => {
|
||||
if (form.payType === 10 && !value) {
|
||||
return Promise.reject('请输入微信号');
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
wechatName: [
|
||||
{
|
||||
validator: (rule: any, value: any) => {
|
||||
if (form.payType === 10 && !value) {
|
||||
return Promise.reject('请输入微信昵称');
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
// 支付宝验证
|
||||
alipayName: [
|
||||
{
|
||||
validator: (rule: any, value: any) => {
|
||||
if (form.payType === 20 && !value) {
|
||||
return Promise.reject('请输入支付宝姓名');
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
alipayAccount: [
|
||||
{
|
||||
validator: (rule: any, value: any) => {
|
||||
if (form.payType === 20 && !value) {
|
||||
return Promise.reject('请输入支付宝账号');
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
// 银行卡验证
|
||||
bankName: [
|
||||
{
|
||||
validator: (rule: any, value: any) => {
|
||||
if (form.payType === 30 && !value) {
|
||||
return Promise.reject('请输入开户行名称');
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
bankAccount: [
|
||||
{
|
||||
validator: (rule: any, value: any) => {
|
||||
if (form.payType === 30 && !value) {
|
||||
return Promise.reject('请输入银行开户名');
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
bankCard: [
|
||||
{
|
||||
validator: (rule: any, value: any) => {
|
||||
if (form.payType === 30 && !value) {
|
||||
return Promise.reject('请输入银行卡号');
|
||||
}
|
||||
if (form.payType === 30 && value && !/^\d{16,19}$/.test(value)) {
|
||||
return Promise.reject('银行卡号格式不正确');
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
applyStatus: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择申请状态',
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
rejectReason: [
|
||||
{
|
||||
validator: (rule: any, value: any) => {
|
||||
if (form.applyStatus === 30 && !value) {
|
||||
return Promise.reject('驳回时必须填写驳回原因');
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
image: [
|
||||
{
|
||||
required: true,
|
||||
message: '请上传打款凭证',
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
/* 打款方式改变时的处理 */
|
||||
const onPayTypeChange = (e: any) => {
|
||||
const payType = e.target.value;
|
||||
|
||||
// 清空其他支付方式的信息
|
||||
if (payType !== 10) {
|
||||
form.alipayAccount = '';
|
||||
form.alipayName = '';
|
||||
}
|
||||
if (payType !== 20) {
|
||||
form.alipayName = '';
|
||||
form.alipayAccount = '';
|
||||
}
|
||||
if (payType !== 30) {
|
||||
form.bankName = '';
|
||||
form.bankAccount = '';
|
||||
form.bankCard = '';
|
||||
}
|
||||
};
|
||||
|
||||
const chooseFile = (data: FileRecord) => {
|
||||
files.value.push({
|
||||
uid: data.id,
|
||||
url: data.url,
|
||||
status: 'done'
|
||||
});
|
||||
form.image = JSON.stringify(files.value.map((d) => d.url));
|
||||
};
|
||||
|
||||
const onDeleteFile = (index: number) => {
|
||||
files.value.splice(index, 1);
|
||||
};
|
||||
|
||||
/* 获取预览提示类型 */
|
||||
const getPreviewAlertType = () => {
|
||||
if (!form.applyStatus) return 'info';
|
||||
|
||||
switch (form.applyStatus) {
|
||||
case 10:
|
||||
return 'processing';
|
||||
case 20:
|
||||
return 'success';
|
||||
case 30:
|
||||
return 'error';
|
||||
case 40:
|
||||
return 'success';
|
||||
default:
|
||||
return 'info';
|
||||
}
|
||||
};
|
||||
|
||||
/* 获取预览文本 */
|
||||
const getPreviewText = () => {
|
||||
if (!form.money || !form.payType) return '';
|
||||
|
||||
const amount = parseFloat(form.money.toString()).toFixed(2);
|
||||
const payTypeMap = {
|
||||
10: '微信',
|
||||
20: '支付宝',
|
||||
30: '银行卡'
|
||||
};
|
||||
const statusMap = {
|
||||
10: '待审核',
|
||||
20: '审核通过',
|
||||
30: '审核驳回',
|
||||
40: '已打款'
|
||||
};
|
||||
|
||||
const payTypeName = payTypeMap[form.payType] || '未知方式';
|
||||
const statusName = statusMap[form.applyStatus] || '未知状态';
|
||||
|
||||
return `提现金额:¥${amount},打款方式:${payTypeName},当前状态:${statusName}`;
|
||||
};
|
||||
|
||||
const { resetFields } = useForm(form, rules);
|
||||
|
||||
/* 保存编辑 */
|
||||
const save = () => {
|
||||
if (!formRef.value) {
|
||||
return;
|
||||
}
|
||||
if (isSuccess.value) {
|
||||
console.log('isSuccess');
|
||||
updateVisible(false);
|
||||
emit('done');
|
||||
return;
|
||||
}
|
||||
if (form.realName == '' || form.realName == null) {
|
||||
message.error('该用户未完成实名认证!');
|
||||
return;
|
||||
}
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(() => {
|
||||
loading.value = true;
|
||||
const formData = {
|
||||
...form
|
||||
};
|
||||
|
||||
// 处理时间字段转换
|
||||
if (formData.auditTime && dayjs.isDayjs(formData.auditTime)) {
|
||||
formData.auditTime = formData.auditTime.valueOf();
|
||||
}
|
||||
|
||||
// 根据支付方式清理不相关字段
|
||||
if (formData.payType !== 10) {
|
||||
delete formData.wechatAccount;
|
||||
delete formData.wechatName;
|
||||
}
|
||||
if (formData.payType !== 20) {
|
||||
delete formData.alipayName;
|
||||
delete formData.alipayAccount;
|
||||
}
|
||||
if (formData.payType !== 30) {
|
||||
delete formData.bankName;
|
||||
delete formData.bankAccount;
|
||||
delete formData.bankCard;
|
||||
}
|
||||
|
||||
const saveOrUpdate = isUpdate.value
|
||||
? updateShopDealerWithdraw
|
||||
: addShopDealerWithdraw;
|
||||
saveOrUpdate(formData)
|
||||
.then((msg) => {
|
||||
loading.value = false;
|
||||
message.success(msg);
|
||||
updateVisible(false);
|
||||
emit('done');
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
message.error(e.message);
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
files.value = [];
|
||||
if (props.data) {
|
||||
assignObject(form, props.data);
|
||||
// 处理时间字段
|
||||
if (props.data.auditTime) {
|
||||
form.auditTime = dayjs(props.data.auditTime);
|
||||
}
|
||||
if (props.data.image) {
|
||||
const arr = JSON.parse(props.data.image);
|
||||
arr.map((url: string) => {
|
||||
files.value.push({
|
||||
uid: uuid(),
|
||||
url: url,
|
||||
status: 'done'
|
||||
});
|
||||
});
|
||||
isSuccess.value = true;
|
||||
}
|
||||
isUpdate.value = true;
|
||||
} else {
|
||||
// 重置为默认值
|
||||
Object.assign(form, {
|
||||
id: undefined,
|
||||
userId: undefined,
|
||||
money: undefined,
|
||||
payType: undefined,
|
||||
wechatAccount: '',
|
||||
wechatName: '',
|
||||
alipayName: '',
|
||||
alipayAccount: '',
|
||||
bankName: '',
|
||||
bankAccount: '',
|
||||
bankCard: '',
|
||||
applyStatus: 10,
|
||||
auditTime: undefined,
|
||||
rejectReason: '',
|
||||
platform: '',
|
||||
image: '',
|
||||
tenantId: undefined,
|
||||
createTime: undefined,
|
||||
updateTime: undefined
|
||||
});
|
||||
isUpdate.value = false;
|
||||
}
|
||||
} else {
|
||||
resetFields();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.platform-option,
|
||||
.status-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.ant-tag {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
span {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.payment-info {
|
||||
background: #fafafa;
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
&.wechat-info {
|
||||
border-left: 3px solid #52c41a;
|
||||
}
|
||||
|
||||
&.alipay-info {
|
||||
border-left: 3px solid #1890ff;
|
||||
}
|
||||
|
||||
&.bank-info {
|
||||
border-left: 3px solid #faad14;
|
||||
}
|
||||
}
|
||||
|
||||
.withdraw-preview {
|
||||
:deep(.ant-alert) {
|
||||
.ant-alert-message {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-divider-horizontal.ant-divider-with-text-left) {
|
||||
margin: 24px 0 16px 0;
|
||||
|
||||
.ant-divider-inner-text {
|
||||
padding: 0 16px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-form-item) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
:deep(.ant-radio) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.ant-radio-inner {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-select-selection-item) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:deep(.ant-input-number) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.ant-alert) {
|
||||
.ant-alert-message {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
464
src/views/shop/shopDealerWithdraw/index.vue
Normal file
464
src/views/shop/shopDealerWithdraw/index.vue
Normal file
@@ -0,0 +1,464 @@
|
||||
<template>
|
||||
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
|
||||
<a-card :bordered="false" :body-style="{ padding: '16px' }">
|
||||
<ele-pro-table
|
||||
ref="tableRef"
|
||||
row-key="shopDealerWithdrawId"
|
||||
:columns="columns"
|
||||
:datasource="datasource"
|
||||
:customRow="customRow"
|
||||
tool-class="ele-toolbar-form"
|
||||
class="sys-org-table"
|
||||
>
|
||||
<template #toolbar>
|
||||
<search
|
||||
@search="reload"
|
||||
:selection="selection"
|
||||
@add="openEdit"
|
||||
@remove="removeBatch"
|
||||
@batchMove="openMove"
|
||||
/>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'applyStatus'">
|
||||
<a-tag v-if="record.applyStatus === 10" color="orange"
|
||||
>待审核</a-tag
|
||||
>
|
||||
<a-tag v-if="record.applyStatus === 20" color="success"
|
||||
>审核通过</a-tag
|
||||
>
|
||||
<a-tag v-if="record.applyStatus === 30" color="error">已驳回</a-tag>
|
||||
<a-tag v-if="record.applyStatus === 40">已打款</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'userInfo'">
|
||||
<a-space>
|
||||
<a-avatar :src="record.avatar" />
|
||||
<div class="flex flex-col">
|
||||
<span>{{ record.realName || '未实名认证' }}</span>
|
||||
<span class="text-gray-400">{{ record.phone }}</span>
|
||||
</div>
|
||||
</a-space>
|
||||
</template>
|
||||
<template v-if="column.key === 'paymentInfo'">
|
||||
<template v-if="record.payType === 10">
|
||||
<a-space direction="vertical">
|
||||
<a-tag color="blue">微信</a-tag>
|
||||
<span>{{ record.wechatName }}</span>
|
||||
<span>{{ record.wechatName }}</span>
|
||||
</a-space>
|
||||
</template>
|
||||
<template v-if="record.payType === 20">
|
||||
<a-space direction="vertical">
|
||||
<a-tag color="blue">支付宝</a-tag>
|
||||
<span>{{ record.alipayName }}</span>
|
||||
<span>{{ record.alipayAccount }}</span>
|
||||
</a-space>
|
||||
</template>
|
||||
<template v-if="record.payType === 30">
|
||||
<a-space direction="vertical">
|
||||
<a-tag color="blue">银行卡</a-tag>
|
||||
<span>{{ record.bankName }}</span>
|
||||
<span>{{ record.bankAccount }}</span>
|
||||
<span>{{ record.bankCard }}</span>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
<template v-if="column.key === 'comments'">
|
||||
<template v-if="record.applyStatus === 30">
|
||||
<div class="text-red-500"
|
||||
>驳回原因:{{ record.rejectReason }}</div
|
||||
>
|
||||
</template>
|
||||
<template v-if="record.applyStatus === 40 && record.image">
|
||||
<a-image
|
||||
v-for="(item, index) in JSON.parse(record.image)"
|
||||
:key="index"
|
||||
:src="item"
|
||||
:width="50"
|
||||
:height="50"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
<template v-if="column.key === 'createTime'">
|
||||
<a-space direction="vertical">
|
||||
<a-tooltip title="创建时间">{{ record.createTime }}</a-tooltip>
|
||||
<a-tooltip title="审核/打款时间" class="text-green-500">{{
|
||||
record.auditTime
|
||||
}}</a-tooltip>
|
||||
</a-space>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<template v-if="record.applyStatus !== 40">
|
||||
<a @click="openEdit(record)" class="ele-text-primary">
|
||||
<EditOutlined />
|
||||
编辑
|
||||
</a>
|
||||
<a-divider type="vertical" />
|
||||
<a-popconfirm
|
||||
title="确定要删除此提现记录吗?"
|
||||
@confirm="remove(record)"
|
||||
placement="topRight"
|
||||
>
|
||||
<a class="ele-text-danger">
|
||||
<DeleteOutlined />
|
||||
删除
|
||||
</a>
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<ShopDealerWithdrawEdit
|
||||
v-model:visible="showEdit"
|
||||
:data="current"
|
||||
@done="reload"
|
||||
/>
|
||||
</a-page-header>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { createVNode, ref } from 'vue';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import {
|
||||
ExclamationCircleOutlined,
|
||||
CheckOutlined,
|
||||
CloseOutlined,
|
||||
DollarOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
import type { EleProTable } from 'ele-admin-pro';
|
||||
import { toDateString } from 'ele-admin-pro';
|
||||
import type {
|
||||
DatasourceFunction,
|
||||
ColumnItem
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import Search from './components/search.vue';
|
||||
import { getPageTitle } from '@/utils/common';
|
||||
import ShopDealerWithdrawEdit from './components/shopDealerWithdrawEdit.vue';
|
||||
import {
|
||||
pageShopDealerWithdraw,
|
||||
removeShopDealerWithdraw,
|
||||
removeBatchShopDealerWithdraw,
|
||||
updateShopDealerWithdraw
|
||||
} from '@/api/shop/shopDealerWithdraw';
|
||||
import type {
|
||||
ShopDealerWithdraw,
|
||||
ShopDealerWithdrawParam
|
||||
} from '@/api/shop/shopDealerWithdraw/model';
|
||||
|
||||
// 表格实例
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
|
||||
// 表格选中数据
|
||||
const selection = ref<ShopDealerWithdraw[]>([]);
|
||||
// 当前编辑数据
|
||||
const current = ref<ShopDealerWithdraw | null>(null);
|
||||
// 是否显示编辑弹窗
|
||||
const showEdit = ref(false);
|
||||
// 是否显示批量移动弹窗
|
||||
const showMove = ref(false);
|
||||
// 加载状态
|
||||
const loading = ref(true);
|
||||
|
||||
// 表格数据源
|
||||
const datasource: DatasourceFunction = ({
|
||||
page,
|
||||
limit,
|
||||
where,
|
||||
orders,
|
||||
filters
|
||||
}) => {
|
||||
if (filters) {
|
||||
where.status = filters.status;
|
||||
}
|
||||
return pageShopDealerWithdraw({
|
||||
...where,
|
||||
...orders,
|
||||
page,
|
||||
limit
|
||||
});
|
||||
};
|
||||
|
||||
// 表格列配置
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
title: '用户ID',
|
||||
dataIndex: 'userId',
|
||||
key: 'userId',
|
||||
align: 'center',
|
||||
width: 90,
|
||||
fixed: 'left'
|
||||
},
|
||||
{
|
||||
title: '提现金额',
|
||||
dataIndex: 'money',
|
||||
key: 'money',
|
||||
align: 'center',
|
||||
width: 150,
|
||||
customRender: ({ text }) => {
|
||||
const amount = parseFloat(text || '0').toFixed(2);
|
||||
return {
|
||||
type: 'span',
|
||||
children: `¥${amount}`
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '用户信息',
|
||||
dataIndex: 'userInfo',
|
||||
key: 'userInfo'
|
||||
},
|
||||
{
|
||||
title: '收款信息',
|
||||
dataIndex: 'paymentInfo',
|
||||
key: 'paymentInfo'
|
||||
},
|
||||
// {
|
||||
// title: '审核时间',
|
||||
// dataIndex: 'auditTime',
|
||||
// key: 'auditTime',
|
||||
// align: 'center',
|
||||
// width: 120,
|
||||
// customRender: ({ text }) => text ? toDateString(new Date(text), 'yyyy-MM-dd HH:mm') : '-'
|
||||
// },
|
||||
// {
|
||||
// title: '驳回原因',
|
||||
// dataIndex: 'rejectReason',
|
||||
// key: 'rejectReason',
|
||||
// align: 'left',
|
||||
// ellipsis: true,
|
||||
// customRender: ({ text }) => text || '-'
|
||||
// },
|
||||
// {
|
||||
// title: '来源平台',
|
||||
// dataIndex: 'platform',
|
||||
// key: 'platform',
|
||||
// align: 'center',
|
||||
// width: 100,
|
||||
// customRender: ({ text }) => text || '-'
|
||||
// },
|
||||
{
|
||||
title: '备注',
|
||||
dataIndex: 'comments',
|
||||
key: 'comments'
|
||||
},
|
||||
{
|
||||
title: '申请状态',
|
||||
dataIndex: 'applyStatus',
|
||||
key: 'applyStatus',
|
||||
align: 'center',
|
||||
width: 150
|
||||
},
|
||||
// {
|
||||
// title: '驳回原因',
|
||||
// dataIndex: 'rejectReason',
|
||||
// key: 'rejectReason',
|
||||
// align: 'center',
|
||||
// },
|
||||
// {
|
||||
// title: '来源客户端',
|
||||
// dataIndex: 'platform',
|
||||
// key: 'platform',
|
||||
// align: 'center',
|
||||
// },
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
align: 'center',
|
||||
width: 180,
|
||||
sorter: true,
|
||||
ellipsis: true,
|
||||
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
|
||||
}
|
||||
// {
|
||||
// title: '操作',
|
||||
// key: 'action',
|
||||
// width: 180,
|
||||
// fixed: 'right',
|
||||
// align: 'center',
|
||||
// hideInSetting: true
|
||||
// }
|
||||
]);
|
||||
|
||||
/* 搜索 */
|
||||
const reload = (where?: ShopDealerWithdrawParam) => {
|
||||
selection.value = [];
|
||||
tableRef?.value?.reload({ where: where });
|
||||
};
|
||||
|
||||
/* 审核通过 */
|
||||
const approveWithdraw = (row: ShopDealerWithdraw) => {
|
||||
Modal.confirm({
|
||||
title: '审核通过确认',
|
||||
content: `已核对信息进行核对,正确无误!`,
|
||||
icon: createVNode(CheckOutlined),
|
||||
okText: '确认通过',
|
||||
okType: 'primary',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
const hide = message.loading('正在处理审核...', 0);
|
||||
// 这里需要调用审核通过的API
|
||||
setTimeout(() => {
|
||||
hide();
|
||||
updateShopDealerWithdraw({
|
||||
id: row.id,
|
||||
applyStatus: 20
|
||||
});
|
||||
message.success('审核通过成功');
|
||||
reload();
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/* 审核驳回 */
|
||||
const rejectWithdraw = (row: ShopDealerWithdraw) => {
|
||||
let rejectReason = '';
|
||||
Modal.confirm({
|
||||
title: '审核驳回',
|
||||
content: createVNode('div', null, [
|
||||
createVNode('p', null, `用户ID: ${row.userId}`),
|
||||
createVNode(
|
||||
'p',
|
||||
null,
|
||||
`提现金额: ¥${parseFloat(row.money || '0').toFixed(2)}`
|
||||
),
|
||||
createVNode('p', { style: 'margin-top: 12px;' }, '请输入驳回原因:'),
|
||||
createVNode('textarea', {
|
||||
placeholder: '请输入驳回原因...',
|
||||
style:
|
||||
'width: 100%; height: 80px; margin-top: 8px; padding: 8px; border: 1px solid #d9d9d9; border-radius: 4px;',
|
||||
onInput: (e: any) => {
|
||||
rejectReason = e.target.value;
|
||||
}
|
||||
})
|
||||
]),
|
||||
icon: createVNode(CloseOutlined),
|
||||
okText: '确认驳回',
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
if (!rejectReason.trim()) {
|
||||
message.error('请输入驳回原因');
|
||||
return Promise.reject();
|
||||
}
|
||||
const hide = message.loading('正在处理审核...', 0);
|
||||
setTimeout(() => {
|
||||
hide();
|
||||
message.success('审核驳回成功');
|
||||
reload();
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/* 确认打款 */
|
||||
const confirmPayment = (row: ShopDealerWithdraw) => {
|
||||
Modal.confirm({
|
||||
title: '确认打款',
|
||||
content: `确定已向用户${row.bankAccount}完成打款?此操作不可撤销`,
|
||||
icon: createVNode(DollarOutlined),
|
||||
okText: '确认打款',
|
||||
okType: 'primary',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
const hide = message.loading('正在确认打款...', 0);
|
||||
setTimeout(() => {
|
||||
updateShopDealerWithdraw({
|
||||
id: row.id,
|
||||
applyStatus: 40
|
||||
});
|
||||
hide();
|
||||
message.success('打款确认成功');
|
||||
reload();
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/* 打开编辑弹窗 */
|
||||
const openEdit = (row?: ShopDealerWithdraw) => {
|
||||
current.value = row ?? null;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
/* 打开批量移动弹窗 */
|
||||
const openMove = () => {
|
||||
showMove.value = true;
|
||||
};
|
||||
|
||||
/* 删除单个 */
|
||||
const remove = (row: ShopDealerWithdraw) => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeShopDealerWithdraw(row.id)
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
/* 批量删除 */
|
||||
const removeBatch = () => {
|
||||
if (!selection.value.length) {
|
||||
message.error('请至少选择一条数据');
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确定要删除选中的记录吗?',
|
||||
icon: createVNode(ExclamationCircleOutlined),
|
||||
maskClosable: true,
|
||||
onOk: () => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeBatchShopDealerWithdraw(selection.value.map((d) => d.id))
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/* 查询 */
|
||||
const query = () => {
|
||||
loading.value = true;
|
||||
};
|
||||
|
||||
/* 自定义行属性 */
|
||||
const customRow = (record: ShopDealerWithdraw) => {
|
||||
return {
|
||||
// 行点击事件
|
||||
onClick: () => {
|
||||
// console.log(record);
|
||||
},
|
||||
// 行双击事件
|
||||
onDblclick: () => {
|
||||
openEdit(record);
|
||||
}
|
||||
};
|
||||
};
|
||||
query();
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ShopDealerWithdraw'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
42
src/views/shop/shopExpress/components/search.vue
Normal file
42
src/views/shop/shopExpress/components/search.vue
Normal 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>
|
||||
222
src/views/shop/shopExpress/components/shopExpressEdit.vue
Normal file
222
src/views/shop/shopExpress/components/shopExpressEdit.vue
Normal file
@@ -0,0 +1,222 @@
|
||||
<!-- 编辑弹窗 -->
|
||||
<template>
|
||||
<ele-modal
|
||||
:width="800"
|
||||
:visible="visible"
|
||||
:maskClosable="false"
|
||||
:maxable="maxable"
|
||||
:title="isUpdate ? '编辑物流公司' : '添加物流公司'"
|
||||
:body-style="{ paddingBottom: '28px' }"
|
||||
@update:visible="updateVisible"
|
||||
@ok="save"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
|
||||
:wrapper-col="
|
||||
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
|
||||
"
|
||||
>
|
||||
<a-form-item label="物流公司名称" name="expressName">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入物流公司名称"
|
||||
v-model:value="form.expressName"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="物流公司编码 (微信)" name="wxCode">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入物流公司编码 (微信)"
|
||||
v-model:value="form.wxCode"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="物流公司编码 (快递100)" name="kuaidi100Code">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入物流公司编码 (快递100)"
|
||||
v-model:value="form.kuaidi100Code"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="物流公司编码 (快递鸟)" name="kdniaoCode">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入物流公司编码 (快递鸟)"
|
||||
v-model:value="form.kdniaoCode"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="排序号" name="sortNumber">
|
||||
<a-input-number
|
||||
:min="0"
|
||||
:max="9999"
|
||||
class="ele-fluid"
|
||||
placeholder="请输入排序号"
|
||||
v-model:value="form.sortNumber"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="是否删除, 0否, 1是" name="deleted">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入是否删除, 0否, 1是"
|
||||
v-model:value="form.deleted"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="修改时间" name="updateTime">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入修改时间"
|
||||
v-model:value="form.updateTime"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { Form, message } from 'ant-design-vue';
|
||||
import { assignObject, uuid } from 'ele-admin-pro';
|
||||
import { addShopExpress, updateShopExpress } from '@/api/shop/shopExpress';
|
||||
import { ShopExpress } from '@/api/shop/shopExpress/model';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
|
||||
import { FormInstance } from 'ant-design-vue/es/form';
|
||||
import { FileRecord } from '@/api/system/file/model';
|
||||
|
||||
// 是否是修改
|
||||
const isUpdate = ref(false);
|
||||
const useForm = Form.useForm;
|
||||
// 是否开启响应式布局
|
||||
const themeStore = useThemeStore();
|
||||
const { styleResponsive } = storeToRefs(themeStore);
|
||||
|
||||
const props = defineProps<{
|
||||
// 弹窗是否打开
|
||||
visible: boolean;
|
||||
// 修改回显的数据
|
||||
data?: ShopExpress | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
// 提交状态
|
||||
const loading = ref(false);
|
||||
// 是否显示最大化切换按钮
|
||||
const maxable = ref(true);
|
||||
// 表格选中数据
|
||||
const formRef = ref<FormInstance | null>(null);
|
||||
const images = ref<ItemType[]>([]);
|
||||
|
||||
// 用户信息
|
||||
const form = reactive<ShopExpress>({
|
||||
expressId: undefined,
|
||||
expressName: undefined,
|
||||
wxCode: undefined,
|
||||
kuaidi100Code: undefined,
|
||||
kdniaoCode: undefined,
|
||||
sortNumber: undefined,
|
||||
deleted: undefined,
|
||||
tenantId: undefined,
|
||||
createTime: undefined,
|
||||
updateTime: undefined,
|
||||
shopExpressId: undefined,
|
||||
shopExpressName: '',
|
||||
status: 0,
|
||||
comments: '',
|
||||
sortNumber: 100
|
||||
});
|
||||
|
||||
/* 更新visible */
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive({
|
||||
shopExpressName: [
|
||||
{
|
||||
required: true,
|
||||
type: 'string',
|
||||
message: '请填写物流公司名称',
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const chooseImage = (data: FileRecord) => {
|
||||
images.value.push({
|
||||
uid: data.id,
|
||||
url: data.path,
|
||||
status: 'done'
|
||||
});
|
||||
form.image = data.path;
|
||||
};
|
||||
|
||||
const onDeleteItem = (index: number) => {
|
||||
images.value.splice(index, 1);
|
||||
form.image = '';
|
||||
};
|
||||
|
||||
const { resetFields } = useForm(form, rules);
|
||||
|
||||
/* 保存编辑 */
|
||||
const save = () => {
|
||||
if (!formRef.value) {
|
||||
return;
|
||||
}
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(() => {
|
||||
loading.value = true;
|
||||
const formData = {
|
||||
...form
|
||||
};
|
||||
const saveOrUpdate = isUpdate.value
|
||||
? updateShopExpress
|
||||
: addShopExpress;
|
||||
saveOrUpdate(formData)
|
||||
.then((msg) => {
|
||||
loading.value = false;
|
||||
message.success(msg);
|
||||
updateVisible(false);
|
||||
emit('done');
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
message.error(e.message);
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
images.value = [];
|
||||
if (props.data) {
|
||||
assignObject(form, props.data);
|
||||
if (props.data.image) {
|
||||
images.value.push({
|
||||
uid: uuid(),
|
||||
url: props.data.image,
|
||||
status: 'done'
|
||||
});
|
||||
}
|
||||
isUpdate.value = true;
|
||||
} else {
|
||||
isUpdate.value = false;
|
||||
}
|
||||
} else {
|
||||
resetFields();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
255
src/views/shop/shopExpress/index.vue
Normal file
255
src/views/shop/shopExpress/index.vue
Normal 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>
|
||||
42
src/views/shop/shopExpressTemplate/components/search.vue
Normal file
42
src/views/shop/shopExpressTemplate/components/search.vue
Normal 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>
|
||||
@@ -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>
|
||||
287
src/views/shop/shopExpressTemplate/index.vue
Normal file
287
src/views/shop/shopExpressTemplate/index.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
299
src/views/shop/shopExpressTemplateDetail/index.vue
Normal file
299
src/views/shop/shopExpressTemplateDetail/index.vue
Normal 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>
|
||||
388
src/views/shop/shopGift/components/makeCard.vue
Normal file
388
src/views/shop/shopGift/components/makeCard.vue
Normal 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>
|
||||
621
src/views/shop/shopGift/components/search.vue
Normal file
621
src/views/shop/shopGift/components/search.vue
Normal 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, '<').replace(/>/g, '>');
|
||||
};
|
||||
|
||||
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>
|
||||
728
src/views/shop/shopGift/components/shopGiftEdit.vue
Normal file
728
src/views/shop/shopGift/components/shopGiftEdit.vue
Normal 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>
|
||||
271
src/views/shop/shopGift/index.vue
Normal file
271
src/views/shop/shopGift/index.vue
Normal 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>
|
||||
38
src/views/shop/shopGoods/components/extra.vue
Normal file
38
src/views/shop/shopGoods/components/extra.vue
Normal 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>
|
||||
158
src/views/shop/shopGoods/components/search.vue
Normal file
158
src/views/shop/shopGoods/components/search.vue
Normal file
@@ -0,0 +1,158 @@
|
||||
<!-- 搜索表单 -->
|
||||
<template>
|
||||
<a-space :size="10" style="flex-wrap: wrap">
|
||||
<a-button type="primary" class="ele-btn-icon" @click="add">
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
<span>添加</span>
|
||||
</a-button>
|
||||
<a-button
|
||||
danger
|
||||
type="primary"
|
||||
class="ele-btn-icon"
|
||||
:disabled="selection?.length === 0"
|
||||
@click="removeBatch"
|
||||
>
|
||||
<template #icon>
|
||||
<DeleteOutlined />
|
||||
</template>
|
||||
<span>批量删除</span>
|
||||
</a-button>
|
||||
<a-radio-group v-model:value="type" @change="handleSearch">
|
||||
<a-radio-button value="出售中"
|
||||
>出售中({{ goodsCount?.totalNum }})
|
||||
</a-radio-button>
|
||||
<a-radio-button value="待上架"
|
||||
>待上架({{ goodsCount?.totalNum2 }})
|
||||
</a-radio-button>
|
||||
<a-radio-button value="已售罄"
|
||||
>已售罄({{ goodsCount?.totalNum3 }})
|
||||
</a-radio-button>
|
||||
</a-radio-group>
|
||||
<a-tree-select
|
||||
allow-clear
|
||||
:tree-data="navigationList"
|
||||
tree-default-expand-all
|
||||
style="width: 240px"
|
||||
:listHeight="700"
|
||||
placeholder="请选择栏目"
|
||||
:value="where.categoryId || undefined"
|
||||
:dropdown-style="{ overflow: 'auto' }"
|
||||
@update:value="(value?: number) => (where.categoryId = value)"
|
||||
@change="onCategoryId"
|
||||
/>
|
||||
<a-input-search
|
||||
allow-clear
|
||||
placeholder="请输入关键词"
|
||||
style="width: 360px"
|
||||
v-model:value="where.keywords"
|
||||
@pressEnter="reload"
|
||||
@search="reload"
|
||||
/>
|
||||
<a-button type="text" @click="reset">重置</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||
import { ref, watch } from 'vue';
|
||||
import { getCount } from '@/api/shop/shopGoods';
|
||||
import type { GoodsCount, ShopGoodsParam } from '@/api/shop/shopGoods/model';
|
||||
import useSearch from '@/utils/use-search';
|
||||
import { getMerchantId } from '@/utils/merchant';
|
||||
import { CmsNavigation } from '@/api/cms/cmsNavigation/model';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
// 选中的角色
|
||||
selection?: [];
|
||||
merchantId?: number;
|
||||
navigationList?: CmsNavigation[];
|
||||
}>(),
|
||||
{
|
||||
merchantId: getMerchantId()
|
||||
}
|
||||
);
|
||||
|
||||
const type = ref<string>();
|
||||
// 统计数据
|
||||
const goodsCount = ref<GoodsCount>();
|
||||
|
||||
// 表单数据
|
||||
const { where, resetFields } = useSearch<ShopGoodsParam>({
|
||||
goodsId: undefined,
|
||||
isShow: undefined,
|
||||
status: undefined,
|
||||
stock: undefined,
|
||||
categoryId: undefined,
|
||||
merchantId: undefined,
|
||||
keywords: ''
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'search', where?: ShopGoodsParam): void;
|
||||
(e: 'add'): void;
|
||||
(e: 'remove'): void;
|
||||
(e: 'batchMove'): void;
|
||||
}>();
|
||||
|
||||
// 新增
|
||||
const add = () => {
|
||||
emit('add');
|
||||
};
|
||||
|
||||
// 批量删除
|
||||
const removeBatch = () => {
|
||||
emit('remove');
|
||||
};
|
||||
|
||||
const handleSearch = (e) => {
|
||||
const text = e.target.value;
|
||||
resetFields();
|
||||
if (text === '出售中') {
|
||||
where.sceneType = 'on_sale';
|
||||
}
|
||||
if (text === '待上架') {
|
||||
where.sceneType = 'pending';
|
||||
}
|
||||
if (text === '已售罄') {
|
||||
where.sceneType = 'sold_out';
|
||||
}
|
||||
emit('search', where);
|
||||
};
|
||||
|
||||
const reload = () => {
|
||||
getCount(where).then((data: any) => {
|
||||
goodsCount.value = data;
|
||||
});
|
||||
emit('search', where);
|
||||
};
|
||||
|
||||
/* 重置 */
|
||||
const reset = () => {
|
||||
resetFields();
|
||||
type.value = '';
|
||||
reload();
|
||||
};
|
||||
|
||||
// 按分类查询
|
||||
const onCategoryId = (id: number) => {
|
||||
where.categoryId = id;
|
||||
emit('search', where);
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.merchantId,
|
||||
(id) => {
|
||||
if (Number(id) > 0) {
|
||||
where.merchantId = id;
|
||||
reload();
|
||||
} else {
|
||||
where.merchantId = undefined;
|
||||
reload();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
1955
src/views/shop/shopGoods/components/shopGoodsEdit.vue
Normal file
1955
src/views/shop/shopGoods/components/shopGoodsEdit.vue
Normal file
File diff suppressed because it is too large
Load Diff
342
src/views/shop/shopGoods/index.vue
Normal file
342
src/views/shop/shopGoods/index.vue
Normal file
@@ -0,0 +1,342 @@
|
||||
<template>
|
||||
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
|
||||
<template #extra>
|
||||
<Extra />
|
||||
</template>
|
||||
<a-card :bordered="false" :body-style="{ padding: '16px' }">
|
||||
<ele-pro-table
|
||||
ref="tableRef"
|
||||
row-key="goodsId"
|
||||
:columns="columns"
|
||||
:datasource="datasource"
|
||||
:customRow="customRow"
|
||||
v-model:selection="selection"
|
||||
tool-class="ele-toolbar-form"
|
||||
class="sys-org-table"
|
||||
>
|
||||
<template #toolbar>
|
||||
<search
|
||||
@search="reload"
|
||||
:selection="selection"
|
||||
:navigationList="navigationList"
|
||||
@add="openEdit"
|
||||
@remove="removeBatch"
|
||||
@batchMove="openMove"
|
||||
/>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'name'">
|
||||
<a-space class="flex items-center cursor-pointer">
|
||||
<a-image
|
||||
:src="record.image"
|
||||
v-if="record.image"
|
||||
:preview="false"
|
||||
:width="50"
|
||||
/>
|
||||
<span class="text-gray-700 font-bold">{{ record.name }}</span>
|
||||
</a-space>
|
||||
</template>
|
||||
<template v-if="column.key === 'recommend'">
|
||||
<a-space @click.stop="onRecommend(record)">
|
||||
<span
|
||||
v-if="record.recommend === 1"
|
||||
class="ele-text-success cursor-pointer"
|
||||
><CheckOutlined
|
||||
/></span>
|
||||
<span v-else class="ele-text-placeholder cursor-pointer"
|
||||
><CloseOutlined
|
||||
/></span>
|
||||
</a-space>
|
||||
</template>
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag v-if="record.status === 0" color="green">出售中</a-tag>
|
||||
<a-tag v-if="record.status === 1" color="orange">待上架</a-tag>
|
||||
<a-tag v-if="record.status === 2" color="purple">待审核</a-tag>
|
||||
<a-tag v-if="record.status === 3" color="red">审核不通过</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a @click.stop="openEdit(record)">修改</a>
|
||||
<a-divider type="vertical" />
|
||||
<a-popconfirm
|
||||
title="确定要删除此记录吗?"
|
||||
@confirm.stop="remove(record)"
|
||||
>
|
||||
<a class="ele-text-danger">删除</a>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<ShopGoodsEdit
|
||||
v-model:visible="showEdit"
|
||||
:navigationList="navigationList"
|
||||
:data="current"
|
||||
@done="reload"
|
||||
/>
|
||||
</a-page-header>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { createVNode, ref } from 'vue';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import {
|
||||
ExclamationCircleOutlined,
|
||||
CheckOutlined,
|
||||
CloseOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
import type { EleProTable } from 'ele-admin-pro';
|
||||
import { toTreeData } from 'ele-admin-pro';
|
||||
import { toDateString } from 'ele-admin-pro';
|
||||
import type {
|
||||
DatasourceFunction,
|
||||
ColumnItem
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import Search from './components/search.vue';
|
||||
import ShopGoodsEdit from './components/shopGoodsEdit.vue';
|
||||
import Extra from '@/views/shop/shopGoods/components/extra.vue';
|
||||
import {
|
||||
pageShopGoods,
|
||||
removeShopGoods,
|
||||
removeBatchShopGoods,
|
||||
updateShopGoods
|
||||
} from '@/api/shop/shopGoods';
|
||||
import type { ShopGoods, ShopGoodsParam } from '@/api/shop/shopGoods/model';
|
||||
import { getPageTitle } from '@/utils/common';
|
||||
import { CmsNavigation } from '@/api/cms/cmsNavigation/model';
|
||||
import { listCmsNavigation } from '@/api/cms/cmsNavigation';
|
||||
|
||||
// 表格实例
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
|
||||
// 表格选中数据
|
||||
const selection = ref<ShopGoods[]>([]);
|
||||
// 当前编辑数据
|
||||
const current = ref<ShopGoods | null>(null);
|
||||
// 栏目数据
|
||||
const navigationList = ref<CmsNavigation[]>();
|
||||
// 是否显示编辑弹窗
|
||||
const showEdit = ref(false);
|
||||
// 是否显示批量移动弹窗
|
||||
const showMove = ref(false);
|
||||
// 加载状态
|
||||
const loading = ref(true);
|
||||
|
||||
// 表格数据源
|
||||
const datasource: DatasourceFunction = ({
|
||||
page,
|
||||
limit,
|
||||
where,
|
||||
orders,
|
||||
filters
|
||||
}) => {
|
||||
if (filters) {
|
||||
where.status = filters.status;
|
||||
}
|
||||
return pageShopGoods({
|
||||
...where,
|
||||
...orders,
|
||||
page,
|
||||
limit
|
||||
});
|
||||
};
|
||||
|
||||
// 表格列配置
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'goodsId',
|
||||
key: 'goodsId',
|
||||
align: 'center',
|
||||
width: 90
|
||||
},
|
||||
{
|
||||
title: '商品',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 300
|
||||
},
|
||||
// {
|
||||
// title: '编号',
|
||||
// dataIndex: 'code',
|
||||
// key: 'code',
|
||||
// align: 'center',
|
||||
// },
|
||||
{
|
||||
title: '价格',
|
||||
dataIndex: 'price',
|
||||
key: 'price',
|
||||
align: 'center',
|
||||
customRender: ({ text }) => `¥${text}`
|
||||
},
|
||||
{
|
||||
title: '销量',
|
||||
dataIndex: 'sales',
|
||||
key: 'sales',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '库存',
|
||||
dataIndex: 'stock',
|
||||
key: 'stock',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '推荐',
|
||||
dataIndex: 'recommend',
|
||||
key: 'recommend',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
align: 'center'
|
||||
},
|
||||
// {
|
||||
// title: '备注',
|
||||
// dataIndex: 'comments',
|
||||
// key: 'comments',
|
||||
// align: 'center',
|
||||
// },
|
||||
{
|
||||
title: '排序号',
|
||||
dataIndex: 'sortNumber',
|
||||
key: 'sortNumber',
|
||||
align: 'center',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
align: 'center',
|
||||
sorter: true,
|
||||
ellipsis: true,
|
||||
width: 180,
|
||||
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
|
||||
}
|
||||
// {
|
||||
// title: '操作',
|
||||
// key: 'action',
|
||||
// width: 180,
|
||||
// fixed: 'right',
|
||||
// align: 'center',
|
||||
// hideInSetting: true
|
||||
// }
|
||||
]);
|
||||
|
||||
/* 搜索 */
|
||||
const reload = (where?: ShopGoodsParam) => {
|
||||
selection.value = [];
|
||||
tableRef?.value?.reload({ where: where });
|
||||
};
|
||||
|
||||
/* 打开编辑弹窗 */
|
||||
const openEdit = (row?: ShopGoods) => {
|
||||
current.value = row ?? null;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
/* 打开批量移动弹窗 */
|
||||
const openMove = () => {
|
||||
showMove.value = true;
|
||||
};
|
||||
|
||||
/* 删除单个 */
|
||||
const remove = (row: ShopGoods) => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeShopGoods(row.goodsId)
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
/* 批量删除 */
|
||||
const removeBatch = () => {
|
||||
if (!selection.value.length) {
|
||||
message.error('请至少选择一条数据');
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确定要删除选中的记录吗?',
|
||||
icon: createVNode(ExclamationCircleOutlined),
|
||||
maskClosable: true,
|
||||
onOk: () => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeBatchShopGoods(selection.value.map((d) => d.goodsId))
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/* 查询 */
|
||||
const query = () => {
|
||||
loading.value = true;
|
||||
// 加载栏目数据
|
||||
if (!navigationList.value) {
|
||||
listCmsNavigation({}).then((res) => {
|
||||
navigationList.value = toTreeData({
|
||||
data: res?.map((d) => {
|
||||
d.value = d.navigationId;
|
||||
d.label = d.title;
|
||||
return d;
|
||||
}),
|
||||
idField: 'navigationId',
|
||||
parentIdField: 'parentId'
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onRecommend = (row: ShopGoods) => {
|
||||
updateShopGoods({
|
||||
...row,
|
||||
recommend: row.recommend == 1 ? 0 : 1
|
||||
}).then((msg) => {
|
||||
message.success(msg);
|
||||
reload();
|
||||
});
|
||||
};
|
||||
|
||||
/* 自定义行属性 */
|
||||
const customRow = (record: ShopGoods) => {
|
||||
return {
|
||||
// 行点击事件
|
||||
onClick: () => {
|
||||
openEdit(record);
|
||||
},
|
||||
// 行双击事件
|
||||
onDblclick: () => {
|
||||
// openEdit(record);
|
||||
}
|
||||
};
|
||||
};
|
||||
query();
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ShopGoods'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
42
src/views/shop/shopGoodsCoupon/components/search.vue
Normal file
42
src/views/shop/shopGoodsCoupon/components/search.vue
Normal 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>
|
||||
@@ -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>
|
||||
270
src/views/shop/shopGoodsCoupon/index.vue
Normal file
270
src/views/shop/shopGoodsCoupon/index.vue
Normal 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>
|
||||
42
src/views/shop/shopGoodsSku/components/search.vue
Normal file
42
src/views/shop/shopGoodsSku/components/search.vue
Normal 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>
|
||||
276
src/views/shop/shopGoodsSku/components/shopGoodsSkuEdit.vue
Normal file
276
src/views/shop/shopGoodsSku/components/shopGoodsSkuEdit.vue
Normal 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>
|
||||
310
src/views/shop/shopGoodsSku/index.vue
Normal file
310
src/views/shop/shopGoodsSku/index.vue
Normal 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>
|
||||
42
src/views/shop/shopGoodsSpec/components/search.vue
Normal file
42
src/views/shop/shopGoodsSpec/components/search.vue
Normal 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>
|
||||
206
src/views/shop/shopGoodsSpec/components/shopGoodsSpecEdit.vue
Normal file
206
src/views/shop/shopGoodsSpec/components/shopGoodsSpecEdit.vue
Normal 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>
|
||||
247
src/views/shop/shopGoodsSpec/index.vue
Normal file
247
src/views/shop/shopGoodsSpec/index.vue
Normal 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>
|
||||
581
src/views/shop/shopMerchant/components/apply.vue
Normal file
581
src/views/shop/shopMerchant/components/apply.vue
Normal 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>
|
||||
56
src/views/shop/shopMerchant/components/extra.vue
Normal file
56
src/views/shop/shopMerchant/components/extra.vue
Normal 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>
|
||||
53
src/views/shop/shopMerchant/components/search.vue
Normal file
53
src/views/shop/shopMerchant/components/search.vue
Normal 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>
|
||||
446
src/views/shop/shopMerchant/components/shopMerchantEdit.vue
Normal file
446
src/views/shop/shopMerchant/components/shopMerchantEdit.vue
Normal 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>
|
||||
277
src/views/shop/shopMerchant/index.vue
Normal file
277
src/views/shop/shopMerchant/index.vue
Normal 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>
|
||||
375
src/views/shop/shopMerchantApply/components/applyReview.vue
Normal file
375
src/views/shop/shopMerchantApply/components/applyReview.vue
Normal file
@@ -0,0 +1,375 @@
|
||||
<template>
|
||||
<ele-modal
|
||||
:width="800"
|
||||
:visible="visible"
|
||||
:maskClosable="false"
|
||||
:maxable="maxable"
|
||||
title="审核商户入驻申请"
|
||||
:body-style="{ paddingBottom: '28px' }"
|
||||
@update:visible="updateVisible"
|
||||
@ok="submitReview"
|
||||
>
|
||||
<a-tabs v-model:activeKey="activeKey">
|
||||
<a-tab-pane key="1" tab="申请信息">
|
||||
<a-descriptions
|
||||
bordered
|
||||
:column="{ xxl: 1, xl: 1, lg: 1, md: 1, sm: 1, xs: 1 }"
|
||||
>
|
||||
<a-descriptions-item label="商户名称">{{
|
||||
data?.merchantName
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="证件号码">{{
|
||||
data?.merchantCode
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="联系人手机号">{{
|
||||
data?.phone
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="联系人姓名">{{
|
||||
data?.realName
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="身份证号码">{{
|
||||
data?.idCard
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="店铺类型">{{
|
||||
data?.shopType
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="商户分类">{{
|
||||
data?.category
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="手续费"
|
||||
>{{ data?.commission }}%</a-descriptions-item
|
||||
>
|
||||
<a-descriptions-item label="创建时间">{{
|
||||
data?.createTime
|
||||
}}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="2" tab="资质文件">
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="8">
|
||||
<div class="file-preview">
|
||||
<h4>营业执照</h4>
|
||||
<a-image
|
||||
v-if="data?.yyzz"
|
||||
:src="data?.yyzz"
|
||||
:width="150"
|
||||
:height="150"
|
||||
/>
|
||||
<a-empty v-else description="未上传" />
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="8">
|
||||
<div class="file-preview">
|
||||
<h4>身份证正面</h4>
|
||||
<a-image
|
||||
v-if="data?.sfz1"
|
||||
:src="data?.sfz1"
|
||||
:width="150"
|
||||
:height="150"
|
||||
/>
|
||||
<a-empty v-else description="未上传" />
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="8">
|
||||
<div class="file-preview">
|
||||
<h4>身份证反面</h4>
|
||||
<a-image
|
||||
v-if="data?.sfz2"
|
||||
:src="data?.sfz2"
|
||||
:width="150"
|
||||
:height="150"
|
||||
/>
|
||||
<a-empty v-else description="未上传" />
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="24" style="margin-top: 20px">
|
||||
<div class="file-preview">
|
||||
<h4>其他资质文件</h4>
|
||||
<div v-if="otherFiles.length > 0" class="files-grid">
|
||||
<a-image
|
||||
v-for="(file, index) in otherFiles"
|
||||
:key="index"
|
||||
:src="file"
|
||||
:width="100"
|
||||
:height="100"
|
||||
style="margin-right: 10px; margin-bottom: 10px"
|
||||
/>
|
||||
</div>
|
||||
<a-empty v-else description="未上传其他资质文件" />
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="3" tab="审核处理">
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="reviewForm"
|
||||
:rules="reviewRules"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
>
|
||||
<a-form-item label="审核结果" name="status" required>
|
||||
<a-radio-group v-model:value="reviewForm.status">
|
||||
<a-radio :value="1">通过</a-radio>
|
||||
<a-radio :value="2">驳回</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
v-if="reviewForm.status === 2"
|
||||
label="驳回原因"
|
||||
name="reason"
|
||||
required
|
||||
>
|
||||
<a-textarea
|
||||
v-model:value="reviewForm.reason"
|
||||
:rows="4"
|
||||
:maxlength="200"
|
||||
placeholder="请输入驳回原因"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
v-if="reviewForm.status === 1"
|
||||
label="手续费(%)"
|
||||
name="commission"
|
||||
>
|
||||
<a-input-number
|
||||
v-model:value="reviewForm.commission"
|
||||
:step="0.1"
|
||||
:max="100"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
placeholder="请输入手续费"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
v-if="reviewForm.status === 1"
|
||||
label="是否自营"
|
||||
name="ownStore"
|
||||
>
|
||||
<a-switch
|
||||
checked-children="是"
|
||||
un-checked-children="否"
|
||||
:checked="reviewForm.ownStore === 1"
|
||||
@update:checked="(val) => (reviewForm.ownStore = val ? 1 : 0)"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
v-if="reviewForm.status === 1"
|
||||
label="是否推荐"
|
||||
name="recommend"
|
||||
>
|
||||
<a-switch
|
||||
checked-children="是"
|
||||
un-checked-children="否"
|
||||
:checked="reviewForm.recommend === 1"
|
||||
@update:checked="(val) => (reviewForm.recommend = val ? 1 : 0)"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
v-if="reviewForm.status === 1"
|
||||
label="是否需要审核"
|
||||
name="goodsReview"
|
||||
>
|
||||
<a-switch
|
||||
checked-children="是"
|
||||
un-checked-children="否"
|
||||
:checked="reviewForm.goodsReview === 1"
|
||||
@update:checked="(val) => (reviewForm.goodsReview = val ? 1 : 0)"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
v-if="reviewForm.status === 1"
|
||||
label="创建商户"
|
||||
name="createMerchant"
|
||||
>
|
||||
<a-switch
|
||||
checked-children="是"
|
||||
un-checked-children="否"
|
||||
:checked="reviewForm.createMerchant"
|
||||
@update:checked="(val) => (reviewForm.createMerchant = val)"
|
||||
/>
|
||||
<div class="tips">审核通过后是否立即创建商户</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="备注" name="comments">
|
||||
<a-textarea
|
||||
v-model:value="reviewForm.comments"
|
||||
:rows="4"
|
||||
:maxlength="200"
|
||||
placeholder="请输入备注信息"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { Form, message } from 'ant-design-vue';
|
||||
import {
|
||||
checkShopMerchantApply,
|
||||
createMerchantFromApply
|
||||
} from '@/api/shop/shopMerchantApply';
|
||||
import { ShopMerchantApply } from '@/api/shop/shopMerchantApply/model';
|
||||
|
||||
const useForm = Form.useForm;
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
data?: ShopMerchantApply | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
// 提交状态
|
||||
const loading = ref(false);
|
||||
// 是否显示最大化切换按钮
|
||||
const maxable = ref(true);
|
||||
// 表单引用
|
||||
const formRef = ref<any>(null);
|
||||
// 当前标签页
|
||||
const activeKey = ref('1');
|
||||
|
||||
// 其他资质文件
|
||||
const otherFiles = ref<string[]>([]);
|
||||
|
||||
// 审核表单
|
||||
const reviewForm = reactive({
|
||||
status: 1, // 默认通过
|
||||
reason: '',
|
||||
commission: 0,
|
||||
ownStore: 0,
|
||||
recommend: 0,
|
||||
goodsReview: 1,
|
||||
createMerchant: true, // 默认创建商户
|
||||
comments: ''
|
||||
});
|
||||
|
||||
// 审核表单验证规则
|
||||
const reviewRules = reactive({
|
||||
status: [{ required: true, message: '请选择审核结果', trigger: 'change' }],
|
||||
reason: [{ required: true, message: '请输入驳回原因', trigger: 'blur' }]
|
||||
});
|
||||
|
||||
/* 更新visible */
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
// 提交审核
|
||||
const submitReview = () => {
|
||||
if (!formRef.value) return;
|
||||
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(() => {
|
||||
loading.value = true;
|
||||
|
||||
const formData = {
|
||||
applyId: props.data?.applyId,
|
||||
status: reviewForm.status,
|
||||
reason: reviewForm.reason,
|
||||
commission: reviewForm.commission,
|
||||
ownStore: reviewForm.ownStore,
|
||||
recommend: reviewForm.recommend,
|
||||
goodsReview: reviewForm.goodsReview,
|
||||
comments: reviewForm.comments
|
||||
};
|
||||
|
||||
checkShopMerchantApply(formData)
|
||||
.then(async (msg) => {
|
||||
message.success(msg);
|
||||
|
||||
// 如果审核通过且需要创建商户
|
||||
if (
|
||||
reviewForm.status === 1 &&
|
||||
reviewForm.createMerchant &&
|
||||
props.data?.applyId
|
||||
) {
|
||||
try {
|
||||
await createMerchantFromApply(props.data.applyId);
|
||||
message.success('商户创建成功');
|
||||
} catch (e) {
|
||||
message.error('商户创建失败: ' + (e as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
updateVisible(false);
|
||||
emit('done');
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
message.error(e.message);
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
const { resetFields } = useForm(reviewForm, reviewRules);
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
resetFields();
|
||||
reviewForm.status = 1;
|
||||
reviewForm.createMerchant = true;
|
||||
|
||||
// 解析其他资质文件
|
||||
otherFiles.value = [];
|
||||
if (props.data?.files) {
|
||||
try {
|
||||
const files = JSON.parse(props.data.files);
|
||||
if (Array.isArray(files)) {
|
||||
otherFiles.value = files;
|
||||
} else {
|
||||
otherFiles.value = [props.data.files];
|
||||
}
|
||||
} catch {
|
||||
otherFiles.value = [props.data.files];
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.file-preview {
|
||||
text-align: center;
|
||||
|
||||
h4 {
|
||||
margin-bottom: 10px;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.files-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tips {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 5px;
|
||||
}
|
||||
</style>
|
||||
143
src/views/shop/shopMerchantApply/components/search.vue
Normal file
143
src/views/shop/shopMerchantApply/components/search.vue
Normal file
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<!-- 搜索表单 -->
|
||||
<a-form
|
||||
:model="searchForm"
|
||||
layout="inline"
|
||||
class="search-form"
|
||||
@finish="handleSearch"
|
||||
>
|
||||
<a-form-item label="商户名称">
|
||||
<a-input
|
||||
v-model:value="searchForm.merchantName"
|
||||
placeholder="请输入商户名称"
|
||||
allow-clear
|
||||
style="width: 160px"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="联系人">
|
||||
<a-input
|
||||
v-model:value="searchForm.realName"
|
||||
placeholder="请输入联系人"
|
||||
allow-clear
|
||||
style="width: 160px"
|
||||
:maxlength="20"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="联系电话">
|
||||
<a-input
|
||||
v-model:value="searchForm.phone"
|
||||
placeholder="请输入联系电话"
|
||||
allow-clear
|
||||
style="width: 160px"
|
||||
:maxlength="11"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="店铺类型">
|
||||
<a-input
|
||||
v-model:value="searchForm.shopType"
|
||||
placeholder="请输入店铺类型"
|
||||
allow-clear
|
||||
style="width: 160px"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="状态">
|
||||
<a-select
|
||||
v-model:value="searchForm.status"
|
||||
placeholder="请选择状态"
|
||||
allow-clear
|
||||
style="width: 120px"
|
||||
>
|
||||
<a-select-option :value="0">待审核</a-select-option>
|
||||
<a-select-option :value="1">审核通过</a-select-option>
|
||||
<a-select-option :value="2">审核驳回</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button type="primary" html-type="submit">
|
||||
<template #icon>
|
||||
<SearchOutlined />
|
||||
</template>
|
||||
查询
|
||||
</a-button>
|
||||
<a-button @click="resetForm">
|
||||
<template #icon>
|
||||
<ReloadOutlined />
|
||||
</template>
|
||||
重置
|
||||
</a-button>
|
||||
<a-button type="primary" @click="emit('add')">
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
新增
|
||||
</a-button>
|
||||
<a-dropdown v-if="selection.length > 0">
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item key="1" @click="emit('remove', selection)">
|
||||
<DeleteOutlined />
|
||||
批量删除
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
<a-button>
|
||||
批量操作
|
||||
<DownOutlined />
|
||||
</a-button>
|
||||
</a-dropdown>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive } from 'vue';
|
||||
import {
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
PlusOutlined,
|
||||
DeleteOutlined,
|
||||
DownOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
|
||||
const emit = defineEmits(['search', 'add', 'remove']);
|
||||
|
||||
// 搜索表单数据
|
||||
const searchForm = reactive({
|
||||
merchantName: '',
|
||||
realName: '',
|
||||
phone: '',
|
||||
shopType: '',
|
||||
status: undefined
|
||||
});
|
||||
|
||||
// 搜索事件
|
||||
const handleSearch = () => {
|
||||
emit('search', { ...searchForm });
|
||||
};
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
Object.assign(searchForm, {
|
||||
merchantName: '',
|
||||
realName: '',
|
||||
phone: '',
|
||||
shopType: '',
|
||||
status: undefined
|
||||
});
|
||||
emit('search', { ...searchForm });
|
||||
};
|
||||
|
||||
defineProps({
|
||||
selection: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,673 @@
|
||||
<template>
|
||||
<ele-modal
|
||||
:width="800"
|
||||
:visible="visible"
|
||||
:maskClosable="false"
|
||||
:maxable="maxable"
|
||||
:title="isUpdate ? '编辑商户入驻申请' : '新增商户入驻申请'"
|
||||
:body-style="{ paddingBottom: '28px' }"
|
||||
@update:visible="updateVisible"
|
||||
@ok="save"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
|
||||
:wrapper-col="
|
||||
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
|
||||
"
|
||||
>
|
||||
<a-tabs v-model:activeKey="activeKey">
|
||||
<a-tab-pane key="1" tab="基本信息">
|
||||
<a-form-item label="商户名称" name="merchantName">
|
||||
<a-input
|
||||
allow-clear
|
||||
:maxlength="100"
|
||||
placeholder="请输入商户名称"
|
||||
v-model:value="form.merchantName"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="证件号码" name="merchantCode">
|
||||
<a-input
|
||||
allow-clear
|
||||
:maxlength="50"
|
||||
placeholder="请输入证件号码"
|
||||
v-model:value="form.merchantCode"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="商户图标" name="image">
|
||||
<div class="flex items-center">
|
||||
<a-image
|
||||
v-if="form.image"
|
||||
:src="form.image"
|
||||
:width="80"
|
||||
:height="80"
|
||||
style="margin-right: 10px"
|
||||
/>
|
||||
<SelectFile
|
||||
:placeholder="`请选择图片`"
|
||||
:limit="1"
|
||||
:data="images"
|
||||
@done="chooseImage"
|
||||
@del="onDeleteImage"
|
||||
/>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
label="商户手机号"
|
||||
name="phone"
|
||||
extra="手机号码将用做于商户端的登录账号请填写真实手机号码"
|
||||
>
|
||||
<a-input
|
||||
allow-clear
|
||||
:maxlength="11"
|
||||
placeholder="请输入商户手机号"
|
||||
v-model:value="form.phone"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="商户姓名" name="realName">
|
||||
<a-input
|
||||
allow-clear
|
||||
:maxlength="100"
|
||||
placeholder="请输入商户姓名"
|
||||
v-model:value="form.realName"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="身份证号码" name="idCard">
|
||||
<a-input
|
||||
allow-clear
|
||||
:maxlength="18"
|
||||
placeholder="请输入身份证号码"
|
||||
v-model:value="form.idCard"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="店铺类型" name="shopType">
|
||||
<a-input
|
||||
allow-clear
|
||||
:maxlength="50"
|
||||
placeholder="请输入店铺类型"
|
||||
v-model:value="form.shopType"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="商户分类" name="category">
|
||||
<a-input
|
||||
allow-clear
|
||||
:maxlength="100"
|
||||
placeholder="请输入商户分类"
|
||||
v-model:value="form.category"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="2" tab="资质信息">
|
||||
<a-form-item label="营业执照" name="yyzz">
|
||||
<div class="flex items-center">
|
||||
<a-image
|
||||
v-if="form.yyzz"
|
||||
:src="form.yyzz"
|
||||
:width="80"
|
||||
:height="80"
|
||||
style="margin-right: 10px"
|
||||
/>
|
||||
<SelectFile
|
||||
:placeholder="`请选择营业执照`"
|
||||
:limit="1"
|
||||
:data="yyzzImages"
|
||||
@done="chooseYyzzImage"
|
||||
@del="onDeleteYyzzImage"
|
||||
/>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="身份证正面" name="sfz1">
|
||||
<div class="flex items-center">
|
||||
<a-image
|
||||
v-if="form.sfz1"
|
||||
:src="form.sfz1"
|
||||
:width="80"
|
||||
:height="80"
|
||||
style="margin-right: 10px"
|
||||
/>
|
||||
<SelectFile
|
||||
:placeholder="`请选择身份证正面`"
|
||||
:limit="1"
|
||||
:data="sfz1Images"
|
||||
@done="chooseSfz1Image"
|
||||
@del="onDeleteSfz1Image"
|
||||
/>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="身份证反面" name="sfz2">
|
||||
<div class="flex items-center">
|
||||
<a-image
|
||||
v-if="form.sfz2"
|
||||
:src="form.sfz2"
|
||||
:width="80"
|
||||
:height="80"
|
||||
style="margin-right: 10px"
|
||||
/>
|
||||
<SelectFile
|
||||
:placeholder="`请选择身份证反面`"
|
||||
:limit="1"
|
||||
:data="sfz2Images"
|
||||
@done="chooseSfz2Image"
|
||||
@del="onDeleteSfz2Image"
|
||||
/>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="资质图片" name="files">
|
||||
<SelectFile
|
||||
:placeholder="`请选择资质图片`"
|
||||
:limit="9"
|
||||
:data="files"
|
||||
@done="chooseFiles"
|
||||
@del="onDeleteFiles"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="3" tab="其他信息">
|
||||
<a-form-item label="手续费(%)" name="commission">
|
||||
<a-input-number
|
||||
:step="0.1"
|
||||
:max="100"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
class="ele-fluid"
|
||||
placeholder="请输入手续费"
|
||||
v-model:value="form.commission"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="关键字" name="keywords">
|
||||
<a-select
|
||||
v-model:value="form.keywords"
|
||||
mode="tags"
|
||||
placeholder="输入关键词后回车"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="是否自营" name="ownStore">
|
||||
<a-switch
|
||||
checked-children="是"
|
||||
un-checked-children="否"
|
||||
:checked="form.ownStore === 1"
|
||||
@update:checked="updateOwnStore"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="是否推荐" name="recommend">
|
||||
<a-switch
|
||||
checked-children="是"
|
||||
un-checked-children="否"
|
||||
:checked="form.recommend === 1"
|
||||
@update:checked="updateRecommend"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="是否需要审核" name="goodsReview">
|
||||
<a-switch
|
||||
checked-children="是"
|
||||
un-checked-children="否"
|
||||
:checked="form.goodsReview === 1"
|
||||
@update:checked="updateGoodsReview"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-radio-group v-model:value="form.status">
|
||||
<a-radio :value="0">待审核</a-radio>
|
||||
<a-radio :value="1">审核通过</a-radio>
|
||||
<a-radio :value="2">审核驳回</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="驳回原因" name="reason" v-if="form.status === 2">
|
||||
<a-textarea
|
||||
:rows="4"
|
||||
:maxlength="200"
|
||||
placeholder="请输入驳回原因"
|
||||
v-model:value="form.reason"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="备注" name="comments">
|
||||
<a-textarea
|
||||
:rows="4"
|
||||
:maxlength="200"
|
||||
placeholder="请输入备注"
|
||||
v-model:value="form.comments"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-form>
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { Form, message } from 'ant-design-vue';
|
||||
import { assignObject, uuid } from 'ele-admin-pro';
|
||||
import {
|
||||
addShopMerchantApply,
|
||||
updateShopMerchantApply
|
||||
} from '@/api/shop/shopMerchantApply';
|
||||
import { ShopMerchantApply } from '@/api/shop/shopMerchantApply/model';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
|
||||
import { FormInstance } from 'ant-design-vue/es/form';
|
||||
import { FileRecord } from '@/api/system/file/model';
|
||||
|
||||
// 是否是修改
|
||||
const isUpdate = ref(false);
|
||||
const useForm = Form.useForm;
|
||||
// 是否开启响应式布局
|
||||
const themeStore = useThemeStore();
|
||||
const { styleResponsive } = storeToRefs(themeStore);
|
||||
|
||||
const props = defineProps<{
|
||||
// 弹窗是否打开
|
||||
visible: boolean;
|
||||
// 修改回显的数据
|
||||
data?: ShopMerchantApply | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
// 提交状态
|
||||
const loading = ref(false);
|
||||
// 是否显示最大化切换按钮
|
||||
const maxable = ref(true);
|
||||
// 表格选中数据
|
||||
const formRef = ref<FormInstance | null>(null);
|
||||
const activeKey = ref('1');
|
||||
|
||||
// 图片数据
|
||||
const images = ref<ItemType[]>([]);
|
||||
const yyzzImages = ref<ItemType[]>([]);
|
||||
const sfz1Images = ref<ItemType[]>([]);
|
||||
const sfz2Images = ref<ItemType[]>([]);
|
||||
const files = ref<ItemType[]>([]);
|
||||
|
||||
// 表单数据
|
||||
const form = reactive<ShopMerchantApply>({
|
||||
applyId: undefined,
|
||||
type: 1,
|
||||
merchantName: undefined,
|
||||
merchantCode: undefined,
|
||||
image: undefined,
|
||||
phone: undefined,
|
||||
realName: undefined,
|
||||
idCard: undefined,
|
||||
shopType: undefined,
|
||||
category: undefined,
|
||||
commission: undefined,
|
||||
keywords: undefined,
|
||||
yyzz: undefined,
|
||||
sfz1: undefined,
|
||||
sfz2: undefined,
|
||||
files: undefined,
|
||||
userId: undefined,
|
||||
ownStore: 0,
|
||||
recommend: 0,
|
||||
goodsReview: 1,
|
||||
name2: undefined,
|
||||
reason: undefined,
|
||||
comments: undefined,
|
||||
status: 0,
|
||||
sortNumber: 100,
|
||||
tenantId: undefined
|
||||
});
|
||||
|
||||
/* 更新visible */
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive({
|
||||
merchantName: [
|
||||
{
|
||||
required: true,
|
||||
type: 'string',
|
||||
message: '请输入商户名称',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
merchantCode: [
|
||||
{
|
||||
required: true,
|
||||
type: 'string',
|
||||
message: '请输入证件号码',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
phone: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入手机号',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
realName: [
|
||||
{
|
||||
required: true,
|
||||
type: 'string',
|
||||
message: '请输入商户姓名',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
idCard: [
|
||||
{
|
||||
required: true,
|
||||
type: 'string',
|
||||
message: '请输入身份证号码',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
shopType: [
|
||||
{
|
||||
required: true,
|
||||
type: 'string',
|
||||
message: '请输入店铺类型',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
category: [
|
||||
{
|
||||
required: true,
|
||||
type: 'string',
|
||||
message: '请输入商户分类',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
yyzz: [
|
||||
{
|
||||
required: true,
|
||||
message: '请上传营业执照',
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
sfz1: [
|
||||
{
|
||||
required: true,
|
||||
message: '请上传身份证正面',
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
sfz2: [
|
||||
{
|
||||
required: true,
|
||||
message: '请上传身份证反面',
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
/* 保存编辑 */
|
||||
const save = () => {
|
||||
if (!formRef.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(() => {
|
||||
loading.value = true;
|
||||
|
||||
// 处理关键字数组转字符串
|
||||
let keywordsStr = '';
|
||||
if (Array.isArray(form.keywords)) {
|
||||
keywordsStr = JSON.stringify(form.keywords);
|
||||
} else if (typeof form.keywords === 'string') {
|
||||
try {
|
||||
JSON.parse(form.keywords);
|
||||
keywordsStr = form.keywords;
|
||||
} catch {
|
||||
keywordsStr = JSON.stringify([form.keywords]);
|
||||
}
|
||||
}
|
||||
|
||||
const formData = {
|
||||
...form,
|
||||
keywords: keywordsStr,
|
||||
files:
|
||||
files.value.length > 0
|
||||
? JSON.stringify(files.value.map((item) => item.url))
|
||||
: undefined
|
||||
};
|
||||
|
||||
const saveOrUpdate = isUpdate.value
|
||||
? updateShopMerchantApply
|
||||
: addShopMerchantApply;
|
||||
saveOrUpdate(formData)
|
||||
.then((msg) => {
|
||||
loading.value = false;
|
||||
message.success(msg);
|
||||
updateVisible(false);
|
||||
emit('done');
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
message.error(e.message);
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
// 图片选择处理
|
||||
const chooseImage = (data: FileRecord) => {
|
||||
images.value = [
|
||||
{
|
||||
uid: data.id,
|
||||
url: data.path,
|
||||
status: 'done'
|
||||
}
|
||||
];
|
||||
form.image = data.path;
|
||||
};
|
||||
|
||||
const onDeleteImage = () => {
|
||||
images.value = [];
|
||||
form.image = undefined;
|
||||
};
|
||||
|
||||
const chooseYyzzImage = (data: FileRecord) => {
|
||||
yyzzImages.value = [
|
||||
{
|
||||
uid: data.id,
|
||||
url: data.path,
|
||||
status: 'done'
|
||||
}
|
||||
];
|
||||
form.yyzz = data.path;
|
||||
};
|
||||
|
||||
const onDeleteYyzzImage = () => {
|
||||
yyzzImages.value = [];
|
||||
form.yyzz = undefined;
|
||||
};
|
||||
|
||||
const chooseSfz1Image = (data: FileRecord) => {
|
||||
sfz1Images.value = [
|
||||
{
|
||||
uid: data.id,
|
||||
url: data.path,
|
||||
status: 'done'
|
||||
}
|
||||
];
|
||||
form.sfz1 = data.path;
|
||||
};
|
||||
|
||||
const onDeleteSfz1Image = () => {
|
||||
sfz1Images.value = [];
|
||||
form.sfz1 = undefined;
|
||||
};
|
||||
|
||||
const chooseSfz2Image = (data: FileRecord) => {
|
||||
sfz2Images.value = [
|
||||
{
|
||||
uid: data.id,
|
||||
url: data.path,
|
||||
status: 'done'
|
||||
}
|
||||
];
|
||||
form.sfz2 = data.path;
|
||||
};
|
||||
|
||||
const onDeleteSfz2Image = () => {
|
||||
sfz2Images.value = [];
|
||||
form.sfz2 = undefined;
|
||||
};
|
||||
|
||||
const chooseFiles = (data: FileRecord) => {
|
||||
files.value.push({
|
||||
uid: data.id,
|
||||
url: data.path,
|
||||
status: 'done'
|
||||
});
|
||||
};
|
||||
|
||||
const onDeleteFiles = (index: number) => {
|
||||
files.value.splice(index, 1);
|
||||
};
|
||||
|
||||
// Switch开关处理
|
||||
const updateOwnStore = (value: boolean) => {
|
||||
form.ownStore = value ? 1 : 0;
|
||||
};
|
||||
|
||||
const updateRecommend = (value: boolean) => {
|
||||
form.recommend = value ? 1 : 0;
|
||||
};
|
||||
|
||||
const updateGoodsReview = (value: boolean) => {
|
||||
form.goodsReview = value ? 1 : 0;
|
||||
};
|
||||
|
||||
const { resetFields } = useForm(form, rules);
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
images.value = [];
|
||||
yyzzImages.value = [];
|
||||
sfz1Images.value = [];
|
||||
sfz2Images.value = [];
|
||||
files.value = [];
|
||||
|
||||
if (props.data) {
|
||||
isUpdate.value = true;
|
||||
assignObject(form, props.data);
|
||||
|
||||
// 处理图片回显
|
||||
if (props.data.image) {
|
||||
images.value.push({
|
||||
uid: uuid(),
|
||||
url: props.data.image,
|
||||
status: 'done'
|
||||
});
|
||||
}
|
||||
|
||||
if (props.data.yyzz) {
|
||||
yyzzImages.value.push({
|
||||
uid: uuid(),
|
||||
url: props.data.yyzz,
|
||||
status: 'done'
|
||||
});
|
||||
}
|
||||
|
||||
if (props.data.sfz1) {
|
||||
sfz1Images.value.push({
|
||||
uid: uuid(),
|
||||
url: props.data.sfz1,
|
||||
status: 'done'
|
||||
});
|
||||
}
|
||||
|
||||
if (props.data.sfz2) {
|
||||
sfz2Images.value.push({
|
||||
uid: uuid(),
|
||||
url: props.data.sfz2,
|
||||
status: 'done'
|
||||
});
|
||||
}
|
||||
|
||||
// 处理关键字回显
|
||||
if (props.data.keywords) {
|
||||
try {
|
||||
form.keywords = JSON.parse(props.data.keywords);
|
||||
} catch {
|
||||
form.keywords = [props.data.keywords];
|
||||
}
|
||||
}
|
||||
|
||||
// 处理资质图片回显
|
||||
if (props.data.files) {
|
||||
try {
|
||||
const fileUrls = JSON.parse(props.data.files);
|
||||
if (Array.isArray(fileUrls)) {
|
||||
fileUrls.forEach((url) => {
|
||||
files.value.push({
|
||||
uid: uuid(),
|
||||
url: url,
|
||||
status: 'done'
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// 如果解析失败,当作单个URL处理
|
||||
files.value.push({
|
||||
uid: uuid(),
|
||||
url: props.data.files,
|
||||
status: 'done'
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
isUpdate.value = false;
|
||||
// 设置默认值
|
||||
form.ownStore = 0;
|
||||
form.recommend = 0;
|
||||
form.goodsReview = 1;
|
||||
form.status = 0;
|
||||
form.sortNumber = 100;
|
||||
}
|
||||
} else {
|
||||
resetFields();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ele-fluid {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
211
src/views/shop/shopMerchantApply/index.vue
Normal file
211
src/views/shop/shopMerchantApply/index.vue
Normal file
@@ -0,0 +1,211 @@
|
||||
<template>
|
||||
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
|
||||
<a-card :bordered="false" :body-style="{ padding: '16px' }">
|
||||
<ele-pro-table
|
||||
ref="tableRef"
|
||||
row-key="applyId"
|
||||
:columns="columns"
|
||||
:datasource="datasource"
|
||||
:customRow="customRow"
|
||||
tool-class="ele-toolbar-form"
|
||||
class="sys-org-table"
|
||||
>
|
||||
<template #toolbar>
|
||||
<Search
|
||||
@search="reload"
|
||||
:selection="selection"
|
||||
@add="openEdit"
|
||||
@remove="removeBatch"
|
||||
/>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'image'">
|
||||
<a-image :src="record.image" :width="50" />
|
||||
</template>
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag v-if="record.status === 0" color="blue">待审核</a-tag>
|
||||
<a-tag v-if="record.status === 1" color="green">审核通过</a-tag>
|
||||
<a-tag v-if="record.status === 2" color="red">审核驳回</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a @click="openEdit(record)">详情</a>
|
||||
<a-divider type="vertical" />
|
||||
<a v-if="record.status === 0" @click="openReview(record)">审核</a>
|
||||
<a-divider v-if="record.status === 0" type="vertical" />
|
||||
<a-popconfirm
|
||||
title="确定要删除此记录吗?"
|
||||
@confirm="remove(record)"
|
||||
>
|
||||
<a class="ele-text-danger">删除</a>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
</a-card>
|
||||
</a-page-header>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<ShopMerchantApplyEdit
|
||||
v-model:visible="showEdit"
|
||||
:data="current"
|
||||
@done="reload"
|
||||
/>
|
||||
|
||||
<!-- 审核弹窗 -->
|
||||
<ApplyReview v-model:visible="showReview" :data="current" @done="reload" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { message } from 'ant-design-vue';
|
||||
import ShopMerchantApplyEdit from './components/shopMerchantApplyEdit.vue';
|
||||
import ApplyReview from './components/applyReview.vue';
|
||||
import Search from './components/search.vue';
|
||||
import {
|
||||
pageShopMerchantApply,
|
||||
removeShopMerchantApply,
|
||||
removeBatchShopMerchantApply
|
||||
} from '@/api/shop/shopMerchantApply';
|
||||
|
||||
// 页面标题
|
||||
const getPageTitle = () => {
|
||||
return '商户入驻申请';
|
||||
};
|
||||
|
||||
const route = useRoute();
|
||||
const tableRef = ref();
|
||||
|
||||
// 表格列配置
|
||||
const columns = reactive([
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'applyId',
|
||||
width: 80,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '商户名称',
|
||||
dataIndex: 'merchantName',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '联系人',
|
||||
dataIndex: 'realName'
|
||||
},
|
||||
{
|
||||
title: '联系电话',
|
||||
dataIndex: 'phone'
|
||||
},
|
||||
{
|
||||
title: '店铺类型',
|
||||
dataIndex: 'shopType'
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
align: 'center',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
align: 'center',
|
||||
width: 150
|
||||
}
|
||||
]);
|
||||
|
||||
// 表格数据源
|
||||
const datasource = ({ page, limit, where }) => {
|
||||
return pageShopMerchantApply({ ...where, page, limit });
|
||||
};
|
||||
|
||||
// 当前选中数据
|
||||
const current = ref(null);
|
||||
|
||||
// 是否显示编辑弹窗
|
||||
const showEdit = ref(false);
|
||||
|
||||
// 是否显示审核弹窗
|
||||
const showReview = ref(false);
|
||||
|
||||
// 表格选中项
|
||||
const selection = ref([]);
|
||||
|
||||
// 刷新表格
|
||||
const reload = (where) => {
|
||||
tableRef.value?.reload({ where });
|
||||
};
|
||||
|
||||
// 打开编辑弹窗
|
||||
const openEdit = (record) => {
|
||||
current.value = record;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
// 打开审核弹窗
|
||||
const openReview = (record) => {
|
||||
current.value = record;
|
||||
showReview.value = true;
|
||||
};
|
||||
|
||||
// 删除单条记录
|
||||
const remove = (record) => {
|
||||
removeShopMerchantApply(record.applyId)
|
||||
.then((msg) => {
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
// 批量删除
|
||||
const removeBatch = (records) => {
|
||||
const ids = records.map((item) => item.applyId);
|
||||
removeBatchShopMerchantApply(ids)
|
||||
.then((msg) => {
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
// 自定义行属性
|
||||
const customRow = (record) => {
|
||||
return {
|
||||
// 行点击事件
|
||||
onClick: () => {
|
||||
// console.log(record);
|
||||
},
|
||||
// 行双击事件
|
||||
onDblclick: () => {
|
||||
openEdit(record);
|
||||
}
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.sys-org-table {
|
||||
:deep(.ant-table-thead > tr > th) {
|
||||
background-color: #fafafa;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:deep(.ant-table-tbody > tr:hover > td) {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
411
src/views/shop/shopOrder/components/deliveryModal.vue
Normal file
411
src/views/shop/shopOrder/components/deliveryModal.vue
Normal file
@@ -0,0 +1,411 @@
|
||||
<!-- 订单发货弹窗 -->
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
title="订单发送货"
|
||||
width="600px"
|
||||
:confirm-loading="loading"
|
||||
@update:visible="updateVisible"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
:label-col="{ span: 4 }"
|
||||
:wrapper-col="{ span: 20 }"
|
||||
>
|
||||
<!-- 配送方式 -->
|
||||
<a-form-item label="配送方式" name="deliveryType">
|
||||
<a-radio-group v-model:value="form.deliveryType">
|
||||
<a-radio :value="0">
|
||||
<span style="color: #1890ff">快递配送</span>
|
||||
</a-radio>
|
||||
<a-radio :value="1">无需发货</a-radio>
|
||||
<a-radio :value="2">商家送货</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 发货类型 -->
|
||||
<a-form-item
|
||||
v-if="form.deliveryType === 0"
|
||||
label="发货类型"
|
||||
name="deliveryMethod"
|
||||
>
|
||||
<a-radio-group v-model:value="form.deliveryMethod">
|
||||
<a-radio value="manual">
|
||||
<span style="color: #1890ff">手动填写</span>
|
||||
</a-radio>
|
||||
<a-radio value="print">电子面单打印</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 快递公司 -->
|
||||
<a-form-item
|
||||
label="快递公司"
|
||||
name="expressId"
|
||||
v-if="form.deliveryType === 0"
|
||||
>
|
||||
<a-row :gutter="8">
|
||||
<a-col :span="18">
|
||||
<a-select
|
||||
v-model:value="form.expressId"
|
||||
placeholder="请选择"
|
||||
show-search
|
||||
:filter-option="filterExpressOption"
|
||||
@change="onExpressChange"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="express in expressList"
|
||||
:key="express.expressId"
|
||||
:value="express.expressId"
|
||||
>
|
||||
{{ express.expressName }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-button type="primary" @click="openExpressModal">
|
||||
设置物流公司
|
||||
</a-button>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
|
||||
<template v-if="form.deliveryType !== 1">
|
||||
<a-form-item label="发货人" name="sendName">
|
||||
<a-input
|
||||
v-model:value="form.sendName"
|
||||
placeholder="请输入发货人"
|
||||
:maxlength="50"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="发货人联系方式" name="sendPhone">
|
||||
<a-input
|
||||
v-model:value="form.sendPhone"
|
||||
placeholder="请输入发货人联系方式"
|
||||
:maxlength="50"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="发货地址" name="sendAddress">
|
||||
<a-input
|
||||
v-model:value="form.sendAddress"
|
||||
placeholder="请输入发货地址"
|
||||
/>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- 快递单号 -->
|
||||
<!-- <a-form-item-->
|
||||
<!-- label="快递单号"-->
|
||||
<!-- name="trackingNumber"-->
|
||||
<!-- v-if="form.deliveryType === 0"-->
|
||||
<!-- >-->
|
||||
<!-- <a-input-->
|
||||
<!-- v-model:value="form.trackingNumber"-->
|
||||
<!-- placeholder="请输入快递单号"-->
|
||||
<!-- :maxlength="50"-->
|
||||
<!-- />-->
|
||||
<!-- </a-form-item>-->
|
||||
|
||||
<!-- <!– 分单发货 –>-->
|
||||
<!-- <a-form-item label="分单发货" v-if="form.deliveryType === 0">-->
|
||||
<!-- <a-switch-->
|
||||
<!-- v-model:checked="form.partialDelivery"-->
|
||||
<!-- checked-children="支持"-->
|
||||
<!-- un-checked-children="不支持"-->
|
||||
<!-- />-->
|
||||
<!-- </a-form-item>-->
|
||||
|
||||
<!-- 发货备注 -->
|
||||
<a-form-item label="发货备注" name="deliveryNote">
|
||||
<a-textarea
|
||||
v-model:value="form.deliveryNote"
|
||||
placeholder="请输入发货备注(可选)"
|
||||
:rows="3"
|
||||
:maxlength="200"
|
||||
show-count
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 发货时间 -->
|
||||
<a-form-item label="发货时间" name="deliveryTime">
|
||||
<a-date-picker
|
||||
v-model:value="form.deliveryTime"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="请选择发货时间"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- 快递公司设置弹窗 -->
|
||||
<express-setting-modal
|
||||
v-model:visible="expressModalVisible"
|
||||
@done="loadExpressList"
|
||||
/>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { Form, message } from 'ant-design-vue';
|
||||
import { ShopOrder } from '@/api/shop/shopOrder/model';
|
||||
import { updateShopOrder } from '@/api/shop/shopOrder';
|
||||
import { listShopExpress } from '@/api/shop/shopExpress';
|
||||
import { ShopExpress } from '@/api/shop/shopExpress/model';
|
||||
import { toDateString } from 'ele-admin-pro';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import ExpressSettingModal from './expressSettingModal.vue';
|
||||
|
||||
const useForm = Form.useForm;
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
data?: ShopOrder | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
(e: 'done'): void;
|
||||
}>();
|
||||
|
||||
// 表单数据
|
||||
const form = reactive({
|
||||
deliveryType: 0, // 0快递配送 1无需发货 2商家送货
|
||||
deliveryMethod: 'manual', // manual手动填写 print电子面单打印
|
||||
expressId: undefined as number | undefined,
|
||||
expressName: '',
|
||||
trackingNumber: '',
|
||||
partialDelivery: false,
|
||||
deliveryNote: '',
|
||||
sendName: '',
|
||||
sendPhone: '',
|
||||
sendAddress: '',
|
||||
deliveryTime: dayjs() as Dayjs
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
deliveryType: [{ required: true, message: '请选择配送方式' }],
|
||||
deliveryMethod: [
|
||||
{
|
||||
validator: (_: any, value: any) => {
|
||||
if (form.deliveryType === 0 && !value) {
|
||||
return Promise.reject('请选择发货类型');
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
],
|
||||
expressId: [
|
||||
{
|
||||
validator: (_: any, value: any) => {
|
||||
if (form.deliveryType === 0 && !value) {
|
||||
return Promise.reject('请选择快递公司');
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
],
|
||||
// trackingNumber: [
|
||||
// {
|
||||
// required: true,
|
||||
// message: '请输入快递单号',
|
||||
// validator: (_: any, value: any) => {
|
||||
// if (form.deliveryType === 0 && !value) {
|
||||
// return Promise.reject('请输入快递单号');
|
||||
// }
|
||||
// return Promise.resolve();
|
||||
// }
|
||||
// }
|
||||
// ],
|
||||
deliveryTime: [{ required: true, message: '请选择发货时间' }],
|
||||
sendName: [
|
||||
{
|
||||
validator: (_: any, value: any) => {
|
||||
if (form.deliveryType !== 1 && !value) {
|
||||
return Promise.reject('请输入发货人');
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
],
|
||||
sendPhone: [
|
||||
{
|
||||
validator: (_: any, value: any) => {
|
||||
if (form.deliveryType !== 1 && !value) {
|
||||
return Promise.reject('请输入发货人联系方式');
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
],
|
||||
sendAddress: [
|
||||
{
|
||||
validator: (_: any, value: any) => {
|
||||
if (form.deliveryType !== 1 && !value) {
|
||||
return Promise.reject('请输入发货地址');
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const formRef = ref();
|
||||
const { resetFields, validate } = useForm(form, rules);
|
||||
|
||||
// 状态
|
||||
const loading = ref(false);
|
||||
const expressList = ref<ShopExpress[]>([]);
|
||||
const expressModalVisible = ref(false);
|
||||
|
||||
// 加载快递公司列表
|
||||
const loadExpressList = async () => {
|
||||
try {
|
||||
const data = await listShopExpress({});
|
||||
expressList.value = data || [];
|
||||
} catch (error) {
|
||||
console.error('加载快递公司失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 快递公司筛选
|
||||
const filterExpressOption = (input: string, option: any) => {
|
||||
return option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
||||
};
|
||||
|
||||
// 快递公司选择变化
|
||||
const onExpressChange = (value: number) => {
|
||||
const express = expressList.value.find((item) => item.expressId === value);
|
||||
if (express) {
|
||||
form.expressName = express.expressName || '';
|
||||
}
|
||||
};
|
||||
|
||||
// 打开快递公司设置
|
||||
const openExpressModal = () => {
|
||||
expressModalVisible.value = true;
|
||||
};
|
||||
|
||||
// 根据配送方式重置不需要的字段
|
||||
watch(
|
||||
() => form.deliveryType,
|
||||
(type) => {
|
||||
if (type !== 0) {
|
||||
form.expressId = undefined;
|
||||
form.expressName = '';
|
||||
form.deliveryMethod = 'manual';
|
||||
}
|
||||
if (type === 1) {
|
||||
form.sendName = '';
|
||||
form.sendPhone = '';
|
||||
form.sendAddress = '';
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 更新弹窗显示状态
|
||||
const updateVisible = (visible: boolean) => {
|
||||
emit('update:visible', visible);
|
||||
};
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
updateVisible(false);
|
||||
};
|
||||
|
||||
// 提交发货
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await validate();
|
||||
loading.value = true;
|
||||
|
||||
const deliveryTime = toDateString(
|
||||
form.deliveryTime.toDate(),
|
||||
'yyyy-MM-dd HH:mm:ss'
|
||||
);
|
||||
|
||||
const updateData = {
|
||||
...props.data,
|
||||
deliveryStatus: 20, // 已发货
|
||||
deliveryType: form.deliveryType,
|
||||
deliveryTime: deliveryTime,
|
||||
deliveryNote: form.deliveryNote
|
||||
};
|
||||
|
||||
// 如果是快递配送,添加快递信息
|
||||
if (form.deliveryType === 0) {
|
||||
updateData.expressId = form.expressId;
|
||||
updateData.sendName = form.sendName;
|
||||
updateData.sendPhone = form.sendPhone;
|
||||
updateData.sendAddress = form.sendAddress;
|
||||
// updateData.expressName = form.expressName;
|
||||
// updateData.trackingNumber = form.trackingNumber;
|
||||
} else if (form.deliveryType === 2) {
|
||||
// 商家送货需要记录发货人信息,但不需要快递公司
|
||||
updateData.sendName = form.sendName;
|
||||
updateData.sendPhone = form.sendPhone;
|
||||
updateData.sendAddress = form.sendAddress;
|
||||
updateData.expressId = undefined;
|
||||
} else {
|
||||
// 无需发货,清理快递/发货信息
|
||||
updateData.expressId = undefined;
|
||||
updateData.sendName = undefined;
|
||||
updateData.sendPhone = undefined;
|
||||
updateData.sendAddress = undefined;
|
||||
}
|
||||
|
||||
// 分单发货
|
||||
if (form.partialDelivery) {
|
||||
updateData.deliveryStatus = 30;
|
||||
}
|
||||
|
||||
await updateShopOrder(updateData);
|
||||
|
||||
message.success('发货成功');
|
||||
emit('done');
|
||||
updateVisible(false);
|
||||
} catch (error: any) {
|
||||
console.log(error);
|
||||
message.error(error.message || '发货失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 监听弹窗显示状态
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
// 重置表单
|
||||
form.deliveryType = 0;
|
||||
form.deliveryMethod = 'manual';
|
||||
form.expressId = undefined;
|
||||
form.expressName = '';
|
||||
form.trackingNumber = '';
|
||||
form.partialDelivery = false;
|
||||
form.deliveryNote = '';
|
||||
form.sendName = '';
|
||||
form.sendPhone = '';
|
||||
form.sendAddress = '';
|
||||
form.deliveryTime = dayjs();
|
||||
|
||||
// 加载快递公司列表
|
||||
loadExpressList();
|
||||
} else {
|
||||
resetFields();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ant-radio-wrapper {
|
||||
margin-right: 16px;
|
||||
}
|
||||
</style>
|
||||
372
src/views/shop/shopOrder/components/expressSettingModal.vue
Normal file
372
src/views/shop/shopOrder/components/expressSettingModal.vue
Normal file
@@ -0,0 +1,372 @@
|
||||
<!-- 快递公司设置弹窗 -->
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
title="设置物流公司"
|
||||
width="900px"
|
||||
:confirm-loading="loading"
|
||||
@update:visible="updateVisible"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<div class="express-setting">
|
||||
<!-- 搜索栏 -->
|
||||
<div class="search-bar" style="margin-bottom: 16px">
|
||||
<a-input-search
|
||||
v-model:value="searchKeyword"
|
||||
placeholder="搜索快递公司名称"
|
||||
style="width: 300px"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
<a-button type="primary" style="margin-left: 8px" @click="openAddModal">
|
||||
添加快递公司
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 快递公司列表 -->
|
||||
<a-table
|
||||
:data-source="filteredExpressList"
|
||||
:columns="columns"
|
||||
:pagination="false"
|
||||
:scroll="{ y: 400 }"
|
||||
row-key="expressId"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-switch
|
||||
v-model:checked="record.enabled"
|
||||
checked-children="启用"
|
||||
un-checked-children="禁用"
|
||||
@change="handleStatusChange(record)"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a @click="editExpress(record)">编辑</a>
|
||||
<a-divider type="vertical" />
|
||||
<a-popconfirm
|
||||
title="确定要删除此快递公司吗?"
|
||||
@confirm="deleteExpress(record)"
|
||||
>
|
||||
<a class="text-red-500">删除</a>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
|
||||
<!-- 添加/编辑快递公司弹窗 -->
|
||||
<a-modal
|
||||
v-model:visible="addModalVisible"
|
||||
:title="editingExpress ? '编辑快递公司' : '添加快递公司'"
|
||||
width="500px"
|
||||
:confirm-loading="addLoading"
|
||||
@ok="handleAddSubmit"
|
||||
@cancel="handleAddCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="addFormRef"
|
||||
:model="addForm"
|
||||
:rules="addRules"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
>
|
||||
<a-form-item label="快递公司名称" name="expressName">
|
||||
<a-input
|
||||
v-model:value="addForm.expressName"
|
||||
placeholder="请输入快递公司名称"
|
||||
:maxlength="50"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="微信编码" name="wxCode">
|
||||
<a-input
|
||||
v-model:value="addForm.wxCode"
|
||||
placeholder="请输入微信快递编码(可选)"
|
||||
:maxlength="20"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="快递100编码" name="kuaidi100Code">
|
||||
<a-input
|
||||
v-model:value="addForm.kuaidi100Code"
|
||||
placeholder="请输入快递100编码(可选)"
|
||||
:maxlength="20"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="快递鸟编码" name="kdniaoCode">
|
||||
<a-input
|
||||
v-model:value="addForm.kdniaoCode"
|
||||
placeholder="请输入快递鸟编码(可选)"
|
||||
:maxlength="20"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="排序号" name="sortNumber">
|
||||
<a-input-number
|
||||
v-model:value="addForm.sortNumber"
|
||||
placeholder="数字越小越靠前"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed, watch } from 'vue';
|
||||
import { Form, message } from 'ant-design-vue';
|
||||
import { ShopExpress } from '@/api/shop/shopExpress/model';
|
||||
import {
|
||||
listShopExpress,
|
||||
addShopExpress,
|
||||
updateShopExpress,
|
||||
removeShopExpress
|
||||
} from '@/api/shop/shopExpress';
|
||||
|
||||
const useForm = Form.useForm;
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
(e: 'done'): void;
|
||||
}>();
|
||||
|
||||
// 状态
|
||||
const loading = ref(false);
|
||||
const searchKeyword = ref('');
|
||||
const expressList = ref<ShopExpress[]>([]);
|
||||
const addModalVisible = ref(false);
|
||||
const addLoading = ref(false);
|
||||
const editingExpress = ref<ShopExpress | null>(null);
|
||||
|
||||
// 添加表单
|
||||
const addForm = reactive({
|
||||
expressName: '',
|
||||
wxCode: '',
|
||||
kuaidi100Code: '',
|
||||
kdniaoCode: '',
|
||||
sortNumber: 0
|
||||
});
|
||||
|
||||
const addRules = {
|
||||
expressName: [
|
||||
{ required: true, message: '请输入快递公司名称' },
|
||||
{ min: 2, max: 50, message: '快递公司名称长度为2-50个字符' }
|
||||
]
|
||||
};
|
||||
|
||||
const addFormRef = ref();
|
||||
const { resetFields: resetAddFields, validate: validateAdd } = useForm(
|
||||
addForm,
|
||||
addRules
|
||||
);
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{
|
||||
title: '快递公司名称',
|
||||
dataIndex: 'expressName',
|
||||
key: 'expressName',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '微信编码',
|
||||
dataIndex: 'wxCode',
|
||||
key: 'wxCode',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '快递100编码',
|
||||
dataIndex: 'kuaidi100Code',
|
||||
key: 'kuaidi100Code',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '快递鸟编码',
|
||||
dataIndex: 'kdniaoCode',
|
||||
key: 'kdniaoCode',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '排序号',
|
||||
dataIndex: 'sortNumber',
|
||||
key: 'sortNumber',
|
||||
width: 80,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 120,
|
||||
align: 'center'
|
||||
}
|
||||
];
|
||||
|
||||
// 过滤后的快递公司列表
|
||||
const filteredExpressList = computed(() => {
|
||||
if (!searchKeyword.value) {
|
||||
return expressList.value;
|
||||
}
|
||||
return expressList.value.filter((item) =>
|
||||
item.expressName
|
||||
?.toLowerCase()
|
||||
.includes(searchKeyword.value.toLowerCase())
|
||||
);
|
||||
});
|
||||
|
||||
// 加载快递公司列表
|
||||
const loadExpressList = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const data = await listShopExpress({});
|
||||
expressList.value = (data || []).map((item) => ({
|
||||
...item,
|
||||
enabled: item.deleted === 0 // 假设deleted=0表示启用
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('加载快递公司失败:', error);
|
||||
message.error('加载快递公司失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
// 搜索逻辑已在computed中处理
|
||||
};
|
||||
|
||||
// 状态变化
|
||||
const handleStatusChange = async (
|
||||
record: ShopExpress & { enabled: boolean }
|
||||
) => {
|
||||
try {
|
||||
await updateShopExpress({
|
||||
...record,
|
||||
deleted: record.enabled ? 0 : 1
|
||||
});
|
||||
message.success(record.enabled ? '已启用' : '已禁用');
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '操作失败');
|
||||
// 恢复状态
|
||||
record.enabled = !record.enabled;
|
||||
}
|
||||
};
|
||||
|
||||
// 打开添加弹窗
|
||||
const openAddModal = () => {
|
||||
editingExpress.value = null;
|
||||
resetAddFields();
|
||||
addModalVisible.value = true;
|
||||
};
|
||||
|
||||
// 编辑快递公司
|
||||
const editExpress = (record: ShopExpress) => {
|
||||
editingExpress.value = record;
|
||||
addForm.expressName = record.expressName || '';
|
||||
addForm.wxCode = record.wxCode || '';
|
||||
addForm.kuaidi100Code = record.kuaidi100Code || '';
|
||||
addForm.kdniaoCode = record.kdniaoCode || '';
|
||||
addForm.sortNumber = record.sortNumber || 0;
|
||||
addModalVisible.value = true;
|
||||
};
|
||||
|
||||
// 删除快递公司
|
||||
const deleteExpress = async (record: ShopExpress) => {
|
||||
try {
|
||||
if (record.expressId) {
|
||||
await removeShopExpress(record.expressId);
|
||||
message.success('删除成功');
|
||||
loadExpressList();
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 添加/编辑提交
|
||||
const handleAddSubmit = async () => {
|
||||
try {
|
||||
await validateAdd();
|
||||
addLoading.value = true;
|
||||
|
||||
if (editingExpress.value) {
|
||||
// 编辑
|
||||
await updateShopExpress({
|
||||
...editingExpress.value,
|
||||
...addForm
|
||||
});
|
||||
message.success('编辑成功');
|
||||
} else {
|
||||
// 添加
|
||||
await addShopExpress(addForm);
|
||||
message.success('添加成功');
|
||||
}
|
||||
|
||||
addModalVisible.value = false;
|
||||
loadExpressList();
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '操作失败');
|
||||
} finally {
|
||||
addLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 取消添加/编辑
|
||||
const handleAddCancel = () => {
|
||||
addModalVisible.value = false;
|
||||
resetAddFields();
|
||||
};
|
||||
|
||||
// 更新弹窗显示状态
|
||||
const updateVisible = (visible: boolean) => {
|
||||
emit('update:visible', visible);
|
||||
};
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
updateVisible(false);
|
||||
};
|
||||
|
||||
// 确定
|
||||
const handleSubmit = () => {
|
||||
emit('done');
|
||||
updateVisible(false);
|
||||
};
|
||||
|
||||
// 监听弹窗显示状态
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
loadExpressList();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.express-setting {
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
1113
src/views/shop/shopOrder/components/orderInfo.vue
Normal file
1113
src/views/shop/shopOrder/components/orderInfo.vue
Normal file
File diff suppressed because it is too large
Load Diff
265
src/views/shop/shopOrder/components/search.vue
Normal file
265
src/views/shop/shopOrder/components/search.vue
Normal file
@@ -0,0 +1,265 @@
|
||||
<!-- 搜索表单 -->
|
||||
<template>
|
||||
<a-space :size="10" style="flex-wrap: wrap">
|
||||
<!-- <a-button-->
|
||||
<!-- danger-->
|
||||
<!-- type="primary"-->
|
||||
<!-- class="ele-btn-icon"-->
|
||||
<!-- :disabled="selection?.length === 0"-->
|
||||
<!-- @click="removeBatch"-->
|
||||
<!-- >-->
|
||||
<!-- <template #icon>-->
|
||||
<!-- <DeleteOutlined/>-->
|
||||
<!-- </template>-->
|
||||
<!-- <span>批量删除</span>-->
|
||||
<!-- </a-button>-->
|
||||
<a-input-search
|
||||
allow-clear
|
||||
v-model:value="where.orderNo"
|
||||
placeholder="订单编号"
|
||||
style="width: 240px"
|
||||
@search="reload"
|
||||
@pressEnter="reload"
|
||||
/>
|
||||
<a-select
|
||||
v-model:value="where.type"
|
||||
style="width: 150px"
|
||||
placeholder="订单类型"
|
||||
@change="search"
|
||||
>
|
||||
<a-select-option value="">全部</a-select-option>
|
||||
<a-select-option :value="1">普通订单</a-select-option>
|
||||
<a-select-option :value="2">秒杀订单</a-select-option>
|
||||
<a-select-option :value="3">拼团订单</a-select-option>
|
||||
</a-select>
|
||||
<a-select
|
||||
v-model:value="where.payStatus"
|
||||
style="width: 150px"
|
||||
placeholder="付款状态"
|
||||
@change="search"
|
||||
>
|
||||
<a-select-option value="">全部</a-select-option>
|
||||
<a-select-option :value="1">已付款</a-select-option>
|
||||
<a-select-option :value="0">未付款</a-select-option>
|
||||
</a-select>
|
||||
<!-- <a-select-->
|
||||
<!-- v-model:value="where.orderStatus"-->
|
||||
<!-- style="width: 150px"-->
|
||||
<!-- placeholder="订单状态"-->
|
||||
<!-- @change="search"-->
|
||||
<!-- >-->
|
||||
<!-- <a-select-option value="">全部</a-select-option>-->
|
||||
<!-- <a-select-option :value="1">已完成</a-select-option>-->
|
||||
<!-- <a-select-option :value="0">未完成</a-select-option>-->
|
||||
<!-- <a-select-option :value="2">未使用</a-select-option>-->
|
||||
<!-- <a-select-option :value="3">已取消</a-select-option>-->
|
||||
<!-- <a-select-option :value="4">退款中</a-select-option>-->
|
||||
<!-- <a-select-option :value="5">退款被拒</a-select-option>-->
|
||||
<!-- <a-select-option :value="6">退款成功</a-select-option>-->
|
||||
<!-- </a-select>-->
|
||||
<a-select
|
||||
:options="getPayType()"
|
||||
v-model:value="where.payType"
|
||||
style="width: 150px"
|
||||
placeholder="付款方式"
|
||||
@change="search"
|
||||
/>
|
||||
|
||||
<a-range-picker
|
||||
v-model:value="dateRange"
|
||||
@change="search"
|
||||
value-format="YYYY-MM-DD"
|
||||
/>
|
||||
<a-input-search
|
||||
allow-clear
|
||||
:placeholder="getSearchPlaceholder()"
|
||||
style="width: 320px"
|
||||
v-model:value="where.keywords"
|
||||
@search="reload"
|
||||
>
|
||||
<template #addonBefore>
|
||||
<a-select v-model:value="type" style="width: 88px" @change="onType">
|
||||
<a-select-option value="">不限</a-select-option>
|
||||
<a-select-option value="userId"> 用户ID </a-select-option>
|
||||
<a-select-option value="phone"> 手机号 </a-select-option>
|
||||
<a-select-option value="nickname"> 昵称 </a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
</a-input-search>
|
||||
<a-button @click="reset">重置</a-button>
|
||||
<a-button @click="handleExport">导出</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { utils, writeFile } from 'xlsx';
|
||||
import { message } from 'ant-design-vue';
|
||||
import useSearch from '@/utils/use-search';
|
||||
import { ShopOrder, ShopOrderParam } from '@/api/shop/shopOrder/model';
|
||||
import { listShopOrder } from '@/api/shop/shopOrder';
|
||||
import { getPayType } from '@/utils/shop';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
// 选中的订单
|
||||
selection?: ShopOrder[];
|
||||
}>(),
|
||||
{}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'search', where?: ShopOrderParam): void;
|
||||
(e: 'add'): void;
|
||||
(e: 'remove'): void;
|
||||
(e: 'batchMove'): void;
|
||||
}>();
|
||||
|
||||
// 表单数据
|
||||
const { where, resetFields } = useSearch<ShopOrderParam>({
|
||||
keywords: '',
|
||||
orderId: undefined,
|
||||
orderNo: undefined,
|
||||
createTimeStart: undefined,
|
||||
createTimeEnd: undefined,
|
||||
userId: undefined,
|
||||
payUserId: undefined,
|
||||
nickname: undefined,
|
||||
phone: undefined,
|
||||
payStatus: undefined,
|
||||
orderStatus: undefined,
|
||||
payType: undefined
|
||||
});
|
||||
|
||||
const reload = () => {
|
||||
emit('search', {
|
||||
...where,
|
||||
keywords: type.value == '' ? where.keywords : undefined
|
||||
});
|
||||
};
|
||||
|
||||
// 批量删除
|
||||
// const removeBatch = () => {
|
||||
// emit('remove');
|
||||
// };
|
||||
|
||||
const onType = () => {
|
||||
resetFields();
|
||||
};
|
||||
|
||||
// 获取搜索框placeholder
|
||||
const getSearchPlaceholder = () => {
|
||||
switch (type.value) {
|
||||
case 'userId':
|
||||
where.userId = Number(where.keywords);
|
||||
return '请输入用户ID';
|
||||
case 'phone':
|
||||
where.phone = where.keywords;
|
||||
return '请输入手机号';
|
||||
case 'nickname':
|
||||
where.nickname = where.keywords;
|
||||
return '请输入用户昵称';
|
||||
default:
|
||||
return '请输入搜索内容';
|
||||
}
|
||||
};
|
||||
|
||||
/* 搜索 */
|
||||
const search = () => {
|
||||
const [d1, d2] = dateRange.value ?? [];
|
||||
xlsFileName.value = `${d1}至${d2}`;
|
||||
where.createTimeStart = d1 ? d1 + ' 00:00:00' : undefined;
|
||||
where.createTimeEnd = d2 ? d2 + ' 23:59:59' : undefined;
|
||||
emit('search', {
|
||||
...where
|
||||
});
|
||||
};
|
||||
|
||||
/* 重置 */
|
||||
const reset = () => {
|
||||
resetFields();
|
||||
search();
|
||||
};
|
||||
|
||||
const dateRange = ref<[string, string]>(['', '']);
|
||||
// 变量
|
||||
const loading = ref(false);
|
||||
const orders = ref<ShopOrder[]>([]);
|
||||
const xlsFileName = ref<string>();
|
||||
const type = ref('');
|
||||
|
||||
// 导出
|
||||
const handleExport = async () => {
|
||||
loading.value = true;
|
||||
const array: (string | number)[][] = [
|
||||
[
|
||||
'订单编号',
|
||||
'订单标题',
|
||||
'买家姓名',
|
||||
'手机号码',
|
||||
'实付金额(元)',
|
||||
'支付方式',
|
||||
'付款时间',
|
||||
'下单时间'
|
||||
]
|
||||
];
|
||||
|
||||
await listShopOrder(where)
|
||||
.then((list) => {
|
||||
orders.value = list;
|
||||
list?.forEach((d: ShopOrder) => {
|
||||
array.push([
|
||||
`${d.orderNo}`,
|
||||
`${d.comments}`,
|
||||
`${d.realName}`,
|
||||
`${d.phone}`,
|
||||
`${d.payPrice}`,
|
||||
`${getPayType(d.payType)}`,
|
||||
`${d.payTime || ''}`,
|
||||
`${d.createTime}`
|
||||
]);
|
||||
});
|
||||
const sheetName = `订单数据`;
|
||||
const workbook = {
|
||||
SheetNames: [sheetName],
|
||||
Sheets: {}
|
||||
};
|
||||
const sheet = utils.aoa_to_sheet(array);
|
||||
workbook.Sheets[sheetName] = sheet;
|
||||
// 设置列宽
|
||||
sheet['!cols'] = [
|
||||
{ wch: 10 },
|
||||
{ wch: 40 },
|
||||
{ wch: 20 },
|
||||
{ wch: 20 },
|
||||
{ wch: 60 },
|
||||
{ wch: 15 },
|
||||
{ wch: 10 },
|
||||
{ wch: 10 },
|
||||
{ wch: 20 },
|
||||
{ wch: 10 },
|
||||
{ wch: 20 }
|
||||
];
|
||||
message.loading('正在导出...');
|
||||
setTimeout(() => {
|
||||
writeFile(
|
||||
workbook,
|
||||
`${
|
||||
where.createTimeEnd ? xlsFileName.value + '_' : ''
|
||||
}${sheetName}.xlsx`
|
||||
);
|
||||
loading.value = false;
|
||||
}, 1000);
|
||||
})
|
||||
.catch((msg) => {
|
||||
message.error(msg);
|
||||
loading.value = false;
|
||||
})
|
||||
.finally(() => {});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.selection,
|
||||
() => {}
|
||||
);
|
||||
</script>
|
||||
726
src/views/shop/shopOrder/index.vue
Normal file
726
src/views/shop/shopOrder/index.vue
Normal file
@@ -0,0 +1,726 @@
|
||||
<template>
|
||||
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
|
||||
<a-card style="margin-bottom: 20px">
|
||||
<search
|
||||
@search="reload"
|
||||
:selection="selection"
|
||||
@add="openEdit"
|
||||
@remove="removeBatch"
|
||||
@batchMove="openMove"
|
||||
/>
|
||||
</a-card>
|
||||
<a-card :bordered="false" :body-style="{ padding: '16px' }">
|
||||
<a-tabs type="card" v-model:activeKey="activeKey" @change="onTabs">
|
||||
<a-tab-pane key="all" tab="全部" />
|
||||
<a-tab-pane key="unpaid" tab="待付款" />
|
||||
<a-tab-pane key="undelivered" tab="待发货" />
|
||||
<a-tab-pane key="unreceived" tab="待收货" />
|
||||
<a-tab-pane key="completed" tab="已完成" />
|
||||
<a-tab-pane key="refunded" tab="退货/售后" />
|
||||
<a-tab-pane key="cancelled" tab="已关闭" />
|
||||
</a-tabs>
|
||||
<ele-pro-table
|
||||
ref="tableRef"
|
||||
row-key="orderId"
|
||||
:columns="columns"
|
||||
:datasource="datasource"
|
||||
:customRow="customRow"
|
||||
:toolbar="false"
|
||||
tool-class="ele-toolbar-form"
|
||||
class="sys-org-table"
|
||||
>
|
||||
<template #toolbar> </template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'name'">
|
||||
<div @click="onSearch(record)" class="cursor-pointer">{{
|
||||
record.name || '匿名'
|
||||
}}</div>
|
||||
</template>
|
||||
<template v-if="column.key === 'orderGoods'">
|
||||
<template v-for="(item, index) in record.orderGoods" :key="index">
|
||||
<div class="item py-1">
|
||||
<a-space :id="`g-${index}`">
|
||||
<a-avatar :src="item.image" shape="square" />
|
||||
<span>{{ item.goodsName }}</span>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<template v-if="column.key === 'phone'">
|
||||
<div v-if="record.mobile" class="text-gray-400">{{
|
||||
record.mobile
|
||||
}}</div>
|
||||
<div v-else class="text-gray-600">{{ record.phone }}</div>
|
||||
</template>
|
||||
<template v-if="column.key === 'payType'">
|
||||
<template v-for="item in getPayType()">
|
||||
<template v-if="record.payStatus == 1">
|
||||
<span v-if="item.value == record.payType">{{
|
||||
item.label
|
||||
}}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span></span>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
<template v-if="column.key === 'payStatus'">
|
||||
<a-tag
|
||||
v-if="record.payStatus"
|
||||
color="green"
|
||||
@click.stop="updatePayStatus(record)"
|
||||
class="cursor-pointer"
|
||||
>已付款
|
||||
</a-tag>
|
||||
<a-tag
|
||||
v-if="!record.payStatus"
|
||||
@click.stop="updatePayStatus(record)"
|
||||
class="cursor-pointer"
|
||||
>未付款
|
||||
</a-tag>
|
||||
<a-tag v-if="record.payStatus == 3">未付款,占场中</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'image'">
|
||||
<a-image :src="record.image" :width="50" />
|
||||
</template>
|
||||
<template v-if="column.key === 'sex'">
|
||||
<a-tag v-if="record.sex === 1">男</a-tag>
|
||||
<a-tag v-if="record.sex === 2">女</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'deliveryStatus'">
|
||||
<a-tag v-if="record.deliveryStatus == 10">未发货</a-tag>
|
||||
<a-tag v-if="record.deliveryStatus == 20" color="green"
|
||||
>已发货</a-tag
|
||||
>
|
||||
<a-tag v-if="record.deliveryStatus == 30" color="blue"
|
||||
>部分发货</a-tag
|
||||
>
|
||||
</template>
|
||||
<template v-if="column.key === 'orderStatus'">
|
||||
<a-tag v-if="record.orderStatus === 0">未完成</a-tag>
|
||||
<a-tag v-if="record.orderStatus === 1" color="green">已完成</a-tag>
|
||||
<a-tag v-if="record.orderStatus === 2">已关闭</a-tag>
|
||||
<a-tag v-if="record.orderStatus === 3" color="red">关闭中</a-tag>
|
||||
<a-tag v-if="record.orderStatus === 4" color="red"
|
||||
>退款申请中</a-tag
|
||||
>
|
||||
<a-tag v-if="record.orderStatus === 5" color="red"
|
||||
>退款被拒绝</a-tag
|
||||
>
|
||||
<a-tag v-if="record.orderStatus === 6" color="orange"
|
||||
>退款成功</a-tag
|
||||
>
|
||||
<a-tag v-if="record.orderStatus === 7" color="pink"
|
||||
>客户端申请退款</a-tag
|
||||
>
|
||||
</template>
|
||||
<template v-if="column.key === 'isInvoice'">
|
||||
<a-tag v-if="record.isInvoice == 0">未开具</a-tag>
|
||||
<a-tag v-if="record.isInvoice == 1" color="green">已开具</a-tag>
|
||||
<a-tag v-if="record.isInvoice == 2" color="blue">不能开具</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag v-if="record.status === 0" color="green">显示</a-tag>
|
||||
<a-tag v-if="record.status === 1" color="red">隐藏</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<!-- 查看详情 - 所有状态都可以查看 -->
|
||||
<a @click.stop="openEdit(record)"> <EyeOutlined /> 详情 </a>
|
||||
|
||||
<!-- 未付款状态的操作 -->
|
||||
<template v-if="!record.payStatus && record.orderStatus === 0">
|
||||
<a-divider type="vertical" />
|
||||
<a @click.stop="handleEditOrder(record)">
|
||||
<EditOutlined /> 修改
|
||||
</a>
|
||||
<a-divider type="vertical" />
|
||||
<a @click.stop="handleCancelOrder(record)">
|
||||
<span class="ele-text-warning"> <CloseOutlined /> 关闭 </span>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<!-- 已付款未发货状态的操作 -->
|
||||
<template
|
||||
v-if="
|
||||
record.payStatus &&
|
||||
record.deliveryStatus === 10 &&
|
||||
!isCancelledStatus(record.orderStatus)
|
||||
"
|
||||
>
|
||||
<a-divider type="vertical" />
|
||||
<a @click.stop="handleDelivery(record)" class="ele-text-primary">
|
||||
<SendOutlined /> 发货
|
||||
</a>
|
||||
<a-divider type="vertical" />
|
||||
<a @click.stop="handleApplyRefund(record)">
|
||||
<UndoOutlined /> 退款
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<!-- 已发货未完成状态的操作 -->
|
||||
<template
|
||||
v-if="
|
||||
record.payStatus &&
|
||||
record.deliveryStatus === 20 &&
|
||||
record.orderStatus === 0
|
||||
"
|
||||
>
|
||||
<a-divider type="vertical" />
|
||||
<a
|
||||
@click.stop="handleConfirmReceive(record)"
|
||||
class="ele-text-primary"
|
||||
>
|
||||
<CheckOutlined /> 确认收货
|
||||
</a>
|
||||
<a-divider type="vertical" />
|
||||
<a @click.stop="handleApplyRefund(record)">
|
||||
<UndoOutlined /> 退款
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<!-- 退款相关状态的操作 -->
|
||||
<template v-if="isRefundStatus(record.orderStatus)">
|
||||
<template
|
||||
v-if="record.orderStatus === 4 || record.orderStatus === 7"
|
||||
>
|
||||
<a-divider type="vertical" />
|
||||
<a
|
||||
@click.stop="handleApproveRefund(record)"
|
||||
class="ele-text-success"
|
||||
>
|
||||
<CheckCircleOutlined /> 同意退款
|
||||
</a>
|
||||
<a-divider type="vertical" />
|
||||
<a
|
||||
@click.stop="handleRejectRefund(record)"
|
||||
class="ele-text-danger"
|
||||
>
|
||||
<CloseCircleOutlined /> 拒绝退款
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<template v-if="record.orderStatus === 5">
|
||||
<a-divider type="vertical" />
|
||||
<a @click.stop="handleRetryRefund(record)">
|
||||
<RedoOutlined /> 重新处理
|
||||
</a>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- 已完成状态的操作 -->
|
||||
<template v-if="record.orderStatus === 1">
|
||||
<a-divider type="vertical" />
|
||||
<a @click.stop="handleApplyRefund(record)">
|
||||
<UndoOutlined /> 申请退款
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<!-- 删除操作 - 已完成、已关闭、退款成功的订单可以删除 -->
|
||||
<template v-if="canDeleteOrder(record)">
|
||||
<a-divider type="vertical" />
|
||||
<a-popconfirm
|
||||
title="确定要删除此订单吗?删除后无法恢复。"
|
||||
@confirm="remove(record)"
|
||||
>
|
||||
<a class="ele-text-danger" @click.stop>
|
||||
<DeleteOutlined /> 删除
|
||||
</a>
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<OrderInfo v-model:visible="showEdit" :data="current" @done="reload" />
|
||||
|
||||
<!-- 发货弹窗 -->
|
||||
<DeliveryModal
|
||||
v-model:visible="showDelivery"
|
||||
:data="current"
|
||||
@done="reload"
|
||||
/>
|
||||
</a-page-header>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { createVNode, ref } from 'vue';
|
||||
import type { EleProTable } from 'ele-admin-pro';
|
||||
import type {
|
||||
DatasourceFunction,
|
||||
ColumnItem
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import {
|
||||
ExclamationCircleOutlined,
|
||||
EyeOutlined,
|
||||
EditOutlined,
|
||||
CloseOutlined,
|
||||
SendOutlined,
|
||||
UndoOutlined,
|
||||
CheckOutlined,
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
RedoOutlined,
|
||||
DeleteOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
import Search from './components/search.vue';
|
||||
import { getPageTitle } from '@/utils/common';
|
||||
import { toDateString } from 'ele-admin-pro';
|
||||
import OrderInfo from './components/orderInfo.vue';
|
||||
import DeliveryModal from './components/deliveryModal.vue';
|
||||
import { ShopOrder, ShopOrderParam } from '@/api/shop/shopOrder/model';
|
||||
import {
|
||||
pageShopOrder,
|
||||
repairOrder,
|
||||
removeShopOrder,
|
||||
removeBatchShopOrder,
|
||||
updateShopOrder
|
||||
} from '@/api/shop/shopOrder';
|
||||
import { updateUser } from '@/api/system/user';
|
||||
import { getPayType } from '@/utils/shop';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
|
||||
// 表格实例
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
|
||||
// 表格选中数据
|
||||
const selection = ref<ShopOrder[]>([]);
|
||||
// 当前编辑数据
|
||||
const current = ref<ShopOrder | null>(null);
|
||||
// 是否显示编辑弹窗
|
||||
const showEdit = ref(false);
|
||||
// 是否显示批量移动弹窗
|
||||
const showMove = ref(false);
|
||||
// 是否显示发货弹窗
|
||||
const showDelivery = ref(false);
|
||||
// 加载状态
|
||||
const loading = ref(true);
|
||||
// 激活的标签
|
||||
const activeKey = ref<string>('all');
|
||||
// 表格数据源
|
||||
const datasource: DatasourceFunction = ({
|
||||
page,
|
||||
limit,
|
||||
where,
|
||||
orders,
|
||||
filters
|
||||
}) => {
|
||||
if (filters) {
|
||||
where.status = filters.status;
|
||||
}
|
||||
where.type = 0;
|
||||
return pageShopOrder({
|
||||
...where,
|
||||
...orders,
|
||||
page,
|
||||
limit
|
||||
});
|
||||
};
|
||||
|
||||
// 表格列配置
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
title: '订单编号',
|
||||
dataIndex: 'orderNo',
|
||||
key: 'orderNo',
|
||||
align: 'center',
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
title: '商品信息',
|
||||
dataIndex: 'orderGoods',
|
||||
key: 'orderGoods',
|
||||
width: 360
|
||||
},
|
||||
{
|
||||
title: '实付金额',
|
||||
dataIndex: 'payPrice',
|
||||
key: 'payPrice',
|
||||
align: 'center',
|
||||
customRender: ({ text }) => '¥' + text
|
||||
},
|
||||
{
|
||||
title: '支付方式',
|
||||
dataIndex: 'payType',
|
||||
key: 'payType',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '支付状态',
|
||||
dataIndex: 'payStatus',
|
||||
key: 'payStatus',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '发货状态',
|
||||
dataIndex: 'deliveryStatus',
|
||||
key: 'deliveryStatus',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '开票状态',
|
||||
dataIndex: 'isInvoice',
|
||||
key: 'isInvoice',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '订单状态',
|
||||
dataIndex: 'orderStatus',
|
||||
key: 'orderStatus',
|
||||
align: 'center'
|
||||
},
|
||||
// {
|
||||
// title: '备注',
|
||||
// dataIndex: 'comments',
|
||||
// key: 'comments',
|
||||
// align: 'center',
|
||||
// },
|
||||
// {
|
||||
// title: '支付时间',
|
||||
// dataIndex: 'payTime',
|
||||
// key: 'payTime',
|
||||
// align: 'center',
|
||||
// width: 180,
|
||||
// sorter: true,
|
||||
// ellipsis: true
|
||||
// },
|
||||
{
|
||||
title: '下单时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
align: 'center',
|
||||
width: 180,
|
||||
sorter: true,
|
||||
ellipsis: true,
|
||||
customRender: ({ text }) => toDateString(text)
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 280,
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
hideInSetting: true
|
||||
}
|
||||
]);
|
||||
|
||||
/* 搜索 */
|
||||
const reload = (where?: ShopOrderParam) => {
|
||||
selection.value = [];
|
||||
tableRef?.value?.reload({ where: where });
|
||||
};
|
||||
|
||||
const onTabs = () => {
|
||||
// 使用statusFilter进行筛选,这是后端专门为订单状态筛选设计的字段
|
||||
const filterParams: Record<string, any> = {};
|
||||
|
||||
// 根据后端 statusFilter 的值对应:
|
||||
// undefined全部,0待付款,1待发货,2待核销,3待收货,4待评价,5已完成,6已退款,7已删除
|
||||
switch (activeKey.value) {
|
||||
case 'all':
|
||||
// 全部订单:不传statusFilter参数
|
||||
// filterParams.statusFilter = undefined; // 不设置该字段
|
||||
break;
|
||||
case 'unpaid':
|
||||
// 待付款:pay_status = false
|
||||
filterParams.statusFilter = 0;
|
||||
break;
|
||||
case 'undelivered':
|
||||
// 待发货:pay_status = true AND delivery_status = 10
|
||||
filterParams.statusFilter = 1;
|
||||
break;
|
||||
case 'unverified':
|
||||
// 待核销:pay_status = true AND delivery_status = 10 (与待发货相同)
|
||||
filterParams.statusFilter = 2;
|
||||
break;
|
||||
case 'unreceived':
|
||||
// 待收货:pay_status = true AND delivery_status = 20
|
||||
filterParams.statusFilter = 3;
|
||||
break;
|
||||
case 'unevaluated':
|
||||
// 待评价:order_status = 1 (与已完成相同)
|
||||
filterParams.statusFilter = 4;
|
||||
break;
|
||||
case 'completed':
|
||||
// 已完成:order_status = 1
|
||||
filterParams.statusFilter = 5;
|
||||
break;
|
||||
case 'cancelled':
|
||||
// 已关闭:order_status = 2
|
||||
filterParams.statusFilter = 8;
|
||||
break;
|
||||
case 'refunded':
|
||||
// 退款/售后:order_status = 6
|
||||
filterParams.statusFilter = 6;
|
||||
break;
|
||||
case 'deleted':
|
||||
// 已删除:deleted = 1
|
||||
filterParams.statusFilter = 7;
|
||||
break;
|
||||
}
|
||||
|
||||
reload(filterParams);
|
||||
};
|
||||
|
||||
const onSearch = (item: ShopOrder) => {
|
||||
reload({ userId: item.userId });
|
||||
};
|
||||
|
||||
/* 打开编辑弹窗 */
|
||||
const openEdit = (row?: ShopOrder) => {
|
||||
current.value = row ?? null;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
/* 打开批量移动弹窗 */
|
||||
const openMove = () => {
|
||||
showMove.value = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 修复订单支付状态
|
||||
*/
|
||||
const updatePayStatus = (record: ShopOrder) => {
|
||||
// 修复订单数据
|
||||
repairOrder({
|
||||
...record
|
||||
}).then(() => {
|
||||
if (!record.realName) {
|
||||
// 更新用户真实姓名
|
||||
updateUser({
|
||||
userId: record.userId,
|
||||
realName: record.realName
|
||||
}).then(() => {});
|
||||
}
|
||||
reload();
|
||||
});
|
||||
};
|
||||
|
||||
/* 查询 */
|
||||
const query = () => {
|
||||
loading.value = true;
|
||||
};
|
||||
|
||||
/* 辅助判断函数 */
|
||||
// 判断是否为关闭状态
|
||||
const isCancelledStatus = (orderStatus?: number) => {
|
||||
return [2, 3].includes(orderStatus || 0);
|
||||
};
|
||||
|
||||
// 判断是否为退款相关状态
|
||||
const isRefundStatus = (orderStatus?: number) => {
|
||||
return [4, 5, 6, 7].includes(orderStatus || 0);
|
||||
};
|
||||
|
||||
// 判断是否可以删除订单
|
||||
const canDeleteOrder = (order: ShopOrder) => {
|
||||
// 已完成、已关闭、退款成功的订单可以删除 (原来是[1, 2, 6],后面改成只有关闭的订单能删除)
|
||||
return [2].includes(order.orderStatus || 0);
|
||||
};
|
||||
|
||||
/* 订单操作方法 */
|
||||
// 修改订单
|
||||
const handleEditOrder = (record: ShopOrder) => {
|
||||
message.info('订单修改功能开发中...');
|
||||
// TODO: 实现订单修改功能
|
||||
};
|
||||
|
||||
// 关闭订单
|
||||
const handleCancelOrder = (record: ShopOrder) => {
|
||||
Modal.confirm({
|
||||
title: '确认关闭订单',
|
||||
content: '确定要关闭此订单吗?关闭后无法恢复。',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await updateShopOrder({
|
||||
...record,
|
||||
orderStatus: 2 // 已关闭
|
||||
});
|
||||
message.success('订单已关闭');
|
||||
reload();
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '关闭订单失败');
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 发货处理
|
||||
const handleDelivery = (record: ShopOrder) => {
|
||||
current.value = record;
|
||||
showDelivery.value = true;
|
||||
};
|
||||
|
||||
// 确认收货
|
||||
const handleConfirmReceive = (record: ShopOrder) => {
|
||||
Modal.confirm({
|
||||
title: '确认收货',
|
||||
content: '确定要将此订单标记为已收货并完成吗?',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await updateShopOrder({
|
||||
...record,
|
||||
deliveryStatus: 30, // 已收货
|
||||
orderStatus: 1 // 已完成
|
||||
});
|
||||
message.success('确认收货成功');
|
||||
reload();
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '确认收货失败');
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 同意退款
|
||||
const handleApproveRefund = (record: ShopOrder) => {
|
||||
Modal.confirm({
|
||||
title: '同意退款',
|
||||
content: '确定要同意此订单的退款申请吗?',
|
||||
onOk: async () => {
|
||||
try {
|
||||
const now = new Date();
|
||||
const refundTime = toDateString(now, 'yyyy-MM-dd HH:mm:ss');
|
||||
|
||||
await updateShopOrder({
|
||||
...record,
|
||||
orderStatus: 6, // 退款成功
|
||||
refundTime: refundTime
|
||||
});
|
||||
message.success('退款处理成功');
|
||||
reload();
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '退款处理失败');
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 拒绝退款
|
||||
const handleRejectRefund = (record: ShopOrder) => {
|
||||
Modal.confirm({
|
||||
title: '拒绝退款',
|
||||
content: '确定要拒绝此订单的退款申请吗?',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await updateShopOrder({
|
||||
...record,
|
||||
orderStatus: 5 // 退款被拒绝
|
||||
});
|
||||
message.success('已拒绝退款申请');
|
||||
reload();
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '操作失败');
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 重新处理退款
|
||||
const handleRetryRefund = (record: ShopOrder) => {
|
||||
Modal.confirm({
|
||||
title: '重新处理退款',
|
||||
content: '确定要重新处理此订单的退款吗?',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await updateShopOrder({
|
||||
...record,
|
||||
orderStatus: 4 // 退款申请中
|
||||
});
|
||||
message.success('已重新提交退款申请');
|
||||
reload();
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '操作失败');
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 申请退款
|
||||
const handleApplyRefund = (record: ShopOrder) => {
|
||||
Modal.confirm({
|
||||
title: '申请退款',
|
||||
content: '确定要为此订单申请退款吗?',
|
||||
onOk: async () => {
|
||||
try {
|
||||
const now = new Date();
|
||||
const refundApplyTime = toDateString(now, 'yyyy-MM-dd HH:mm:ss');
|
||||
|
||||
await updateShopOrder({
|
||||
...record,
|
||||
orderStatus: 4, // 退款申请中
|
||||
refundApplyTime: refundApplyTime
|
||||
});
|
||||
message.success('退款申请已提交');
|
||||
reload();
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '申请退款失败');
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/* 删除单个订单 */
|
||||
const remove = (row: ShopOrder) => {
|
||||
removeShopOrder(row.orderId)
|
||||
.then(() => {
|
||||
message.success('删除成功');
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
/* 批量删除订单 */
|
||||
const removeBatch = () => {
|
||||
if (!selection.value.length) {
|
||||
message.error('请至少选择一条数据');
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确定要删除选中的记录吗?',
|
||||
icon: createVNode(ExclamationCircleOutlined),
|
||||
maskClosable: true,
|
||||
onOk: () => {
|
||||
const ids = selection.value.map((d) => d.orderId);
|
||||
removeBatchShopOrder(ids)
|
||||
.then(() => {
|
||||
message.success('删除成功');
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
message.error(e.message);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/* 自定义行属性 */
|
||||
const customRow = (record: ShopOrder) => {
|
||||
return {
|
||||
// 行点击事件
|
||||
onClick: () => {
|
||||
openEdit(record);
|
||||
},
|
||||
// 行双击事件
|
||||
onDblclick: () => {
|
||||
// openEdit(record);
|
||||
}
|
||||
};
|
||||
};
|
||||
query();
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import * as MenuIcons from '@/layout/menu-icons';
|
||||
|
||||
export default {
|
||||
name: 'BszxOrder',
|
||||
components: MenuIcons
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
42
src/views/shop/shopOrderGoods/components/search.vue
Normal file
42
src/views/shop/shopOrderGoods/components/search.vue
Normal 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>
|
||||
359
src/views/shop/shopOrderGoods/components/shopOrderGoodsEdit.vue
Normal file
359
src/views/shop/shopOrderGoods/components/shopOrderGoodsEdit.vue
Normal file
@@ -0,0 +1,359 @@
|
||||
<!-- 编辑弹窗 -->
|
||||
<template>
|
||||
<ele-modal
|
||||
:width="800"
|
||||
:visible="visible"
|
||||
:maskClosable="false"
|
||||
:maxable="maxable"
|
||||
:title="isUpdate ? '编辑商品信息' : '添加商品信息'"
|
||||
:body-style="{ paddingBottom: '28px' }"
|
||||
@update:visible="updateVisible"
|
||||
@ok="save"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
|
||||
:wrapper-col="
|
||||
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
|
||||
"
|
||||
>
|
||||
<a-form-item label="关联订单表id" name="orderId">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入关联订单表id"
|
||||
v-model:value="form.orderId"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="订单标识" name="orderCode">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入订单标识"
|
||||
v-model:value="form.orderCode"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="关联商户ID" name="merchantId">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入关联商户ID"
|
||||
v-model:value="form.merchantId"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="商户名称" name="merchantName">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入商户名称"
|
||||
v-model:value="form.merchantName"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="商品封面图" name="image">
|
||||
<SelectFile
|
||||
:placeholder="`请选择图片`"
|
||||
:limit="1"
|
||||
:data="images"
|
||||
@done="chooseImage"
|
||||
@del="onDeleteItem"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="关联商品id" name="goodsId">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入关联商品id"
|
||||
v-model:value="form.goodsId"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="商品名称" name="goodsName">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入商品名称"
|
||||
v-model:value="form.goodsName"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="商品规格" name="spec">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入商品规格"
|
||||
v-model:value="form.spec"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="" name="skuId">
|
||||
<a-input allow-clear placeholder="请输入" v-model:value="form.skuId" />
|
||||
</a-form-item>
|
||||
<a-form-item label="单价" name="price">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入单价"
|
||||
v-model:value="form.price"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="购买数量" name="totalNum">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入购买数量"
|
||||
v-model:value="form.totalNum"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
label="0 未付款 1已付款,2无需付款或占用状态"
|
||||
name="payStatus"
|
||||
>
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入0 未付款 1已付款,2无需付款或占用状态"
|
||||
v-model:value="form.payStatus"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
label="0未使用,1已完成,2已取消,3取消中,4退款申请中,5退款被拒绝,6退款成功,7客户端申请退款"
|
||||
name="orderStatus"
|
||||
>
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入0未使用,1已完成,2已取消,3取消中,4退款申请中,5退款被拒绝,6退款成功,7客户端申请退款"
|
||||
v-model:value="form.orderStatus"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="是否免费:0收费、1免费" name="isFree">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入是否免费:0收费、1免费"
|
||||
v-model:value="form.isFree"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="系统版本 0当前版本 其他版本" name="version">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入系统版本 0当前版本 其他版本"
|
||||
v-model:value="form.version"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="预约时间段" name="timePeriod">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入预约时间段"
|
||||
v-model:value="form.timePeriod"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="预定日期" name="dateTime">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入预定日期"
|
||||
v-model:value="form.dateTime"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="开场时间" name="startTime">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入开场时间"
|
||||
v-model:value="form.startTime"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="结束时间" name="endTime">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入结束时间"
|
||||
v-model:value="form.endTime"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="毫秒时间戳" name="timeFlag">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入毫秒时间戳"
|
||||
v-model:value="form.timeFlag"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="过期时间" name="expirationTime">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入过期时间"
|
||||
v-model:value="form.expirationTime"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="备注" name="comments">
|
||||
<a-textarea
|
||||
:rows="4"
|
||||
:maxlength="200"
|
||||
placeholder="请输入描述"
|
||||
v-model:value="form.comments"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="用户id" name="userId">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入用户id"
|
||||
v-model:value="form.userId"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="更新时间" name="updateTime">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入更新时间"
|
||||
v-model:value="form.updateTime"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { Form, message } from 'ant-design-vue';
|
||||
import { assignObject, uuid } from 'ele-admin-pro';
|
||||
import {
|
||||
addShopOrderGoods,
|
||||
updateShopOrderGoods
|
||||
} from '@/api/shop/shopOrderGoods';
|
||||
import { ShopOrderGoods } from '@/api/shop/shopOrderGoods/model';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
|
||||
import { FormInstance } from 'ant-design-vue/es/form';
|
||||
import { FileRecord } from '@/api/system/file/model';
|
||||
|
||||
// 是否是修改
|
||||
const isUpdate = ref(false);
|
||||
const useForm = Form.useForm;
|
||||
// 是否开启响应式布局
|
||||
const themeStore = useThemeStore();
|
||||
const { styleResponsive } = storeToRefs(themeStore);
|
||||
|
||||
const props = defineProps<{
|
||||
// 弹窗是否打开
|
||||
visible: boolean;
|
||||
// 修改回显的数据
|
||||
data?: ShopOrderGoods | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
// 提交状态
|
||||
const loading = ref(false);
|
||||
// 是否显示最大化切换按钮
|
||||
const maxable = ref(true);
|
||||
// 表格选中数据
|
||||
const formRef = ref<FormInstance | null>(null);
|
||||
const images = ref<ItemType[]>([]);
|
||||
|
||||
// 用户信息
|
||||
const form = reactive<ShopOrderGoods>({
|
||||
id: undefined,
|
||||
orderId: undefined,
|
||||
orderCode: undefined,
|
||||
merchantId: undefined,
|
||||
merchantName: undefined,
|
||||
image: undefined,
|
||||
goodsId: undefined,
|
||||
goodsName: undefined,
|
||||
spec: undefined,
|
||||
skuId: undefined,
|
||||
price: undefined,
|
||||
totalNum: undefined,
|
||||
payStatus: undefined,
|
||||
orderStatus: undefined,
|
||||
isFree: undefined,
|
||||
version: undefined,
|
||||
timePeriod: undefined,
|
||||
dateTime: undefined,
|
||||
startTime: undefined,
|
||||
endTime: undefined,
|
||||
timeFlag: undefined,
|
||||
expirationTime: undefined,
|
||||
comments: undefined,
|
||||
userId: undefined,
|
||||
tenantId: undefined,
|
||||
updateTime: undefined,
|
||||
createTime: undefined
|
||||
});
|
||||
|
||||
/* 更新visible */
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive({
|
||||
shopOrderGoodsName: [
|
||||
{
|
||||
required: true,
|
||||
type: 'string',
|
||||
message: '请填写商品信息名称',
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const chooseImage = (data: FileRecord) => {
|
||||
images.value.push({
|
||||
uid: data.id,
|
||||
url: data.path,
|
||||
status: 'done'
|
||||
});
|
||||
form.image = data.path;
|
||||
};
|
||||
|
||||
const onDeleteItem = (index: number) => {
|
||||
images.value.splice(index, 1);
|
||||
form.image = '';
|
||||
};
|
||||
|
||||
const { resetFields } = useForm(form, rules);
|
||||
|
||||
/* 保存编辑 */
|
||||
const save = () => {
|
||||
if (!formRef.value) {
|
||||
return;
|
||||
}
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(() => {
|
||||
loading.value = true;
|
||||
const formData = {
|
||||
...form
|
||||
};
|
||||
const saveOrUpdate = isUpdate.value
|
||||
? updateShopOrderGoods
|
||||
: addShopOrderGoods;
|
||||
saveOrUpdate(formData)
|
||||
.then((msg) => {
|
||||
loading.value = false;
|
||||
message.success(msg);
|
||||
updateVisible(false);
|
||||
emit('done');
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
message.error(e.message);
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
images.value = [];
|
||||
if (props.data) {
|
||||
assignObject(form, props.data);
|
||||
if (props.data.image) {
|
||||
images.value.push({
|
||||
uid: uuid(),
|
||||
url: props.data.image,
|
||||
status: 'done'
|
||||
});
|
||||
}
|
||||
isUpdate.value = true;
|
||||
} else {
|
||||
isUpdate.value = false;
|
||||
}
|
||||
} else {
|
||||
resetFields();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
373
src/views/shop/shopOrderGoods/index.vue
Normal file
373
src/views/shop/shopOrderGoods/index.vue
Normal file
@@ -0,0 +1,373 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="ele-body">
|
||||
<a-card :bordered="false" :body-style="{ padding: '16px' }">
|
||||
<ele-pro-table
|
||||
ref="tableRef"
|
||||
row-key="shopOrderGoodsId"
|
||||
:columns="columns"
|
||||
:datasource="datasource"
|
||||
:customRow="customRow"
|
||||
tool-class="ele-toolbar-form"
|
||||
class="sys-org-table"
|
||||
>
|
||||
<template #toolbar>
|
||||
<search
|
||||
@search="reload"
|
||||
:selection="selection"
|
||||
@add="openEdit"
|
||||
@remove="removeBatch"
|
||||
@batchMove="openMove"
|
||||
/>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'image'">
|
||||
<a-image :src="record.image" :width="50" />
|
||||
</template>
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag v-if="record.status === 0" color="green">显示</a-tag>
|
||||
<a-tag v-if="record.status === 1" color="red">隐藏</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a @click="openEdit(record)">修改</a>
|
||||
<a-divider type="vertical" />
|
||||
<a-popconfirm
|
||||
title="确定要删除此记录吗?"
|
||||
@confirm="remove(record)"
|
||||
>
|
||||
<a class="ele-text-danger">删除</a>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<ShopOrderGoodsEdit
|
||||
v-model:visible="showEdit"
|
||||
:data="current"
|
||||
@done="reload"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { createVNode, ref } from 'vue';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
|
||||
import type { EleProTable } from 'ele-admin-pro';
|
||||
import { toDateString } from 'ele-admin-pro';
|
||||
import type {
|
||||
DatasourceFunction,
|
||||
ColumnItem
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import Search from './components/search.vue';
|
||||
import ShopOrderGoodsEdit from './components/shopOrderGoodsEdit.vue';
|
||||
import {
|
||||
pageShopOrderGoods,
|
||||
removeShopOrderGoods,
|
||||
removeBatchShopOrderGoods
|
||||
} from '@/api/shop/shopOrderGoods';
|
||||
import type {
|
||||
ShopOrderGoods,
|
||||
ShopOrderGoodsParam
|
||||
} from '@/api/shop/shopOrderGoods/model';
|
||||
|
||||
// 表格实例
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
|
||||
// 表格选中数据
|
||||
const selection = ref<ShopOrderGoods[]>([]);
|
||||
// 当前编辑数据
|
||||
const current = ref<ShopOrderGoods | null>(null);
|
||||
// 是否显示编辑弹窗
|
||||
const showEdit = ref(false);
|
||||
// 是否显示批量移动弹窗
|
||||
const showMove = ref(false);
|
||||
// 加载状态
|
||||
const loading = ref(true);
|
||||
|
||||
// 表格数据源
|
||||
const datasource: DatasourceFunction = ({
|
||||
page,
|
||||
limit,
|
||||
where,
|
||||
orders,
|
||||
filters
|
||||
}) => {
|
||||
if (filters) {
|
||||
where.status = filters.status;
|
||||
}
|
||||
return pageShopOrderGoods({
|
||||
...where,
|
||||
...orders,
|
||||
page,
|
||||
limit
|
||||
});
|
||||
};
|
||||
|
||||
// 表格列配置
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
title: '自增ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
align: 'center',
|
||||
width: 90
|
||||
},
|
||||
{
|
||||
title: '关联订单表id',
|
||||
dataIndex: 'orderId',
|
||||
key: 'orderId',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '订单标识',
|
||||
dataIndex: 'orderCode',
|
||||
key: 'orderCode',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '关联商户ID',
|
||||
dataIndex: 'merchantId',
|
||||
key: 'merchantId',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '商户名称',
|
||||
dataIndex: 'merchantName',
|
||||
key: 'merchantName',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '商品封面图',
|
||||
dataIndex: 'image',
|
||||
key: 'image',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '关联商品id',
|
||||
dataIndex: 'goodsId',
|
||||
key: 'goodsId',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '商品名称',
|
||||
dataIndex: 'goodsName',
|
||||
key: 'goodsName',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '商品规格',
|
||||
dataIndex: 'spec',
|
||||
key: 'spec',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'skuId',
|
||||
key: 'skuId',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '单价',
|
||||
dataIndex: 'price',
|
||||
key: 'price',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '购买数量',
|
||||
dataIndex: 'totalNum',
|
||||
key: 'totalNum',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '0 未付款 1已付款,2无需付款或占用状态',
|
||||
dataIndex: 'payStatus',
|
||||
key: 'payStatus',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title:
|
||||
'0未使用,1已完成,2已取消,3取消中,4退款申请中,5退款被拒绝,6退款成功,7客户端申请退款',
|
||||
dataIndex: 'orderStatus',
|
||||
key: 'orderStatus',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '是否免费:0收费、1免费',
|
||||
dataIndex: 'isFree',
|
||||
key: 'isFree',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '系统版本 0当前版本 其他版本',
|
||||
dataIndex: 'version',
|
||||
key: 'version',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '预约时间段',
|
||||
dataIndex: 'timePeriod',
|
||||
key: 'timePeriod',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '预定日期',
|
||||
dataIndex: 'dateTime',
|
||||
key: 'dateTime',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '开场时间',
|
||||
dataIndex: 'startTime',
|
||||
key: 'startTime',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '结束时间',
|
||||
dataIndex: 'endTime',
|
||||
key: 'endTime',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '毫秒时间戳',
|
||||
dataIndex: 'timeFlag',
|
||||
key: 'timeFlag',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '过期时间',
|
||||
dataIndex: 'expirationTime',
|
||||
key: 'expirationTime',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
dataIndex: 'comments',
|
||||
key: 'comments',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '用户id',
|
||||
dataIndex: 'userId',
|
||||
key: 'userId',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '更新时间',
|
||||
dataIndex: 'updateTime',
|
||||
key: 'updateTime',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
align: 'center',
|
||||
sorter: true,
|
||||
ellipsis: true,
|
||||
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd')
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 180,
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
hideInSetting: true
|
||||
}
|
||||
]);
|
||||
|
||||
/* 搜索 */
|
||||
const reload = (where?: ShopOrderGoodsParam) => {
|
||||
selection.value = [];
|
||||
tableRef?.value?.reload({ where: where });
|
||||
};
|
||||
|
||||
/* 打开编辑弹窗 */
|
||||
const openEdit = (row?: ShopOrderGoods) => {
|
||||
current.value = row ?? null;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
/* 打开批量移动弹窗 */
|
||||
const openMove = () => {
|
||||
showMove.value = true;
|
||||
};
|
||||
|
||||
/* 删除单个 */
|
||||
const remove = (row: ShopOrderGoods) => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeShopOrderGoods(row.shopOrderGoodsId)
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
/* 批量删除 */
|
||||
const removeBatch = () => {
|
||||
if (!selection.value.length) {
|
||||
message.error('请至少选择一条数据');
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确定要删除选中的记录吗?',
|
||||
icon: createVNode(ExclamationCircleOutlined),
|
||||
maskClosable: true,
|
||||
onOk: () => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeBatchShopOrderGoods(
|
||||
selection.value.map((d) => d.shopOrderGoodsId)
|
||||
)
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/* 查询 */
|
||||
const query = () => {
|
||||
loading.value = true;
|
||||
};
|
||||
|
||||
/* 自定义行属性 */
|
||||
const customRow = (record: ShopOrderGoods) => {
|
||||
return {
|
||||
// 行点击事件
|
||||
onClick: () => {
|
||||
// console.log(record);
|
||||
},
|
||||
// 行双击事件
|
||||
onDblclick: () => {
|
||||
openEdit(record);
|
||||
}
|
||||
};
|
||||
};
|
||||
query();
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ShopOrderGoods'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
42
src/views/shop/shopSpec/components/search.vue
Normal file
42
src/views/shop/shopSpec/components/search.vue
Normal 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>
|
||||
223
src/views/shop/shopSpec/components/shopSpecEdit.vue
Normal file
223
src/views/shop/shopSpec/components/shopSpecEdit.vue
Normal file
@@ -0,0 +1,223 @@
|
||||
<!-- 编辑弹窗 -->
|
||||
<template>
|
||||
<ele-modal
|
||||
:width="800"
|
||||
:visible="visible"
|
||||
:maskClosable="false"
|
||||
:maxable="maxable"
|
||||
:title="isUpdate ? '编辑规格' : '添加规格'"
|
||||
:body-style="{ paddingBottom: '28px' }"
|
||||
@update:visible="updateVisible"
|
||||
@ok="save"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
|
||||
:wrapper-col="
|
||||
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
|
||||
"
|
||||
>
|
||||
<a-form-item label="规格名称" name="specName">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入规格名称"
|
||||
v-model:value="form.specName"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="规格值" name="specValue">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入规格值"
|
||||
v-model:value="form.specValue"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="商户ID" name="merchantId">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入商户ID"
|
||||
v-model:value="form.merchantId"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="创建用户" name="userId">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入创建用户"
|
||||
v-model:value="form.userId"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="更新者" name="updater">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入更新者"
|
||||
v-model:value="form.updater"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="备注" name="comments">
|
||||
<a-textarea
|
||||
:rows="4"
|
||||
:maxlength="200"
|
||||
placeholder="请输入描述"
|
||||
v-model:value="form.comments"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-radio-group v-model:value="form.status">
|
||||
<a-radio :value="0">显示</a-radio>
|
||||
<a-radio :value="1">隐藏</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="排序号" name="sortNumber">
|
||||
<a-input-number
|
||||
:min="0"
|
||||
:max="9999"
|
||||
class="ele-fluid"
|
||||
placeholder="请输入排序号"
|
||||
v-model:value="form.sortNumber"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { Form, message } from 'ant-design-vue';
|
||||
import { assignObject, uuid } from 'ele-admin-pro';
|
||||
import { addShopSpec, updateShopSpec } from '@/api/shop/shopSpec';
|
||||
import { ShopSpec } from '@/api/shop/shopSpec/model';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
|
||||
import { FormInstance } from 'ant-design-vue/es/form';
|
||||
import { FileRecord } from '@/api/system/file/model';
|
||||
|
||||
// 是否是修改
|
||||
const isUpdate = ref(false);
|
||||
const useForm = Form.useForm;
|
||||
// 是否开启响应式布局
|
||||
const themeStore = useThemeStore();
|
||||
const { styleResponsive } = storeToRefs(themeStore);
|
||||
|
||||
const props = defineProps<{
|
||||
// 弹窗是否打开
|
||||
visible: boolean;
|
||||
// 修改回显的数据
|
||||
data?: ShopSpec | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
// 提交状态
|
||||
const loading = ref(false);
|
||||
// 是否显示最大化切换按钮
|
||||
const maxable = ref(true);
|
||||
// 表格选中数据
|
||||
const formRef = ref<FormInstance | null>(null);
|
||||
const images = ref<ItemType[]>([]);
|
||||
|
||||
// 用户信息
|
||||
const form = reactive<ShopSpec>({
|
||||
specId: undefined,
|
||||
specName: undefined,
|
||||
specValue: undefined,
|
||||
merchantId: undefined,
|
||||
userId: undefined,
|
||||
updater: undefined,
|
||||
comments: undefined,
|
||||
status: undefined,
|
||||
sortNumber: undefined,
|
||||
tenantId: undefined,
|
||||
createTime: undefined
|
||||
});
|
||||
|
||||
/* 更新visible */
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive({
|
||||
shopSpecName: [
|
||||
{
|
||||
required: true,
|
||||
type: 'string',
|
||||
message: '请填写规格名称',
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const chooseImage = (data: FileRecord) => {
|
||||
images.value.push({
|
||||
uid: data.id,
|
||||
url: data.path,
|
||||
status: 'done'
|
||||
});
|
||||
form.image = data.path;
|
||||
};
|
||||
|
||||
const onDeleteItem = (index: number) => {
|
||||
images.value.splice(index, 1);
|
||||
form.image = '';
|
||||
};
|
||||
|
||||
const { resetFields } = useForm(form, rules);
|
||||
|
||||
/* 保存编辑 */
|
||||
const save = () => {
|
||||
if (!formRef.value) {
|
||||
return;
|
||||
}
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(() => {
|
||||
loading.value = true;
|
||||
const formData = {
|
||||
...form
|
||||
};
|
||||
const saveOrUpdate = isUpdate.value ? updateShopSpec : addShopSpec;
|
||||
saveOrUpdate(formData)
|
||||
.then((msg) => {
|
||||
loading.value = false;
|
||||
message.success(msg);
|
||||
updateVisible(false);
|
||||
emit('done');
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
message.error(e.message);
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
images.value = [];
|
||||
if (props.data) {
|
||||
assignObject(form, props.data);
|
||||
if (props.data.image) {
|
||||
images.value.push({
|
||||
uid: uuid(),
|
||||
url: props.data.image,
|
||||
status: 'done'
|
||||
});
|
||||
}
|
||||
isUpdate.value = true;
|
||||
} else {
|
||||
isUpdate.value = false;
|
||||
}
|
||||
} else {
|
||||
resetFields();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
267
src/views/shop/shopSpec/index.vue
Normal file
267
src/views/shop/shopSpec/index.vue
Normal file
@@ -0,0 +1,267 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="ele-body">
|
||||
<a-card :bordered="false" :body-style="{ padding: '16px' }">
|
||||
<ele-pro-table
|
||||
ref="tableRef"
|
||||
row-key="shopSpecId"
|
||||
:columns="columns"
|
||||
:datasource="datasource"
|
||||
:customRow="customRow"
|
||||
tool-class="ele-toolbar-form"
|
||||
class="sys-org-table"
|
||||
>
|
||||
<template #toolbar>
|
||||
<search
|
||||
@search="reload"
|
||||
:selection="selection"
|
||||
@add="openEdit"
|
||||
@remove="removeBatch"
|
||||
@batchMove="openMove"
|
||||
/>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'image'">
|
||||
<a-image :src="record.image" :width="50" />
|
||||
</template>
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag v-if="record.status === 0" color="green">显示</a-tag>
|
||||
<a-tag v-if="record.status === 1" color="red">隐藏</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a @click="openEdit(record)">修改</a>
|
||||
<a-divider type="vertical" />
|
||||
<a-popconfirm
|
||||
title="确定要删除此记录吗?"
|
||||
@confirm="remove(record)"
|
||||
>
|
||||
<a class="ele-text-danger">删除</a>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<ShopSpecEdit v-model:visible="showEdit" :data="current" @done="reload" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { createVNode, ref } from 'vue';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
|
||||
import type { EleProTable } from 'ele-admin-pro';
|
||||
import { toDateString } from 'ele-admin-pro';
|
||||
import type {
|
||||
DatasourceFunction,
|
||||
ColumnItem
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import Search from './components/search.vue';
|
||||
import ShopSpecEdit from './components/shopSpecEdit.vue';
|
||||
import {
|
||||
pageShopSpec,
|
||||
removeShopSpec,
|
||||
removeBatchShopSpec
|
||||
} from '@/api/shop/shopSpec';
|
||||
import type { ShopSpec, ShopSpecParam } from '@/api/shop/shopSpec/model';
|
||||
|
||||
// 表格实例
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
|
||||
// 表格选中数据
|
||||
const selection = ref<ShopSpec[]>([]);
|
||||
// 当前编辑数据
|
||||
const current = ref<ShopSpec | null>(null);
|
||||
// 是否显示编辑弹窗
|
||||
const showEdit = ref(false);
|
||||
// 是否显示批量移动弹窗
|
||||
const showMove = ref(false);
|
||||
// 加载状态
|
||||
const loading = ref(true);
|
||||
|
||||
// 表格数据源
|
||||
const datasource: DatasourceFunction = ({
|
||||
page,
|
||||
limit,
|
||||
where,
|
||||
orders,
|
||||
filters
|
||||
}) => {
|
||||
if (filters) {
|
||||
where.status = filters.status;
|
||||
}
|
||||
return pageShopSpec({
|
||||
...where,
|
||||
...orders,
|
||||
page,
|
||||
limit
|
||||
});
|
||||
};
|
||||
|
||||
// 表格列配置
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
title: '规格ID',
|
||||
dataIndex: 'specId',
|
||||
key: 'specId',
|
||||
align: 'center',
|
||||
width: 90
|
||||
},
|
||||
{
|
||||
title: '规格名称',
|
||||
dataIndex: 'specName',
|
||||
key: 'specName',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '规格值',
|
||||
dataIndex: 'specValue',
|
||||
key: 'specValue',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '商户ID',
|
||||
dataIndex: 'merchantId',
|
||||
key: 'merchantId',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '创建用户',
|
||||
dataIndex: 'userId',
|
||||
key: 'userId',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '更新者',
|
||||
dataIndex: 'updater',
|
||||
key: 'updater',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
dataIndex: 'comments',
|
||||
key: 'comments',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '状态, 0正常, 1待修,2异常已修,3异常未修',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '排序号',
|
||||
dataIndex: 'sortNumber',
|
||||
key: 'sortNumber',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
align: 'center',
|
||||
sorter: true,
|
||||
ellipsis: true,
|
||||
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd')
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 180,
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
hideInSetting: true
|
||||
}
|
||||
]);
|
||||
|
||||
/* 搜索 */
|
||||
const reload = (where?: ShopSpecParam) => {
|
||||
selection.value = [];
|
||||
tableRef?.value?.reload({ where: where });
|
||||
};
|
||||
|
||||
/* 打开编辑弹窗 */
|
||||
const openEdit = (row?: ShopSpec) => {
|
||||
current.value = row ?? null;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
/* 打开批量移动弹窗 */
|
||||
const openMove = () => {
|
||||
showMove.value = true;
|
||||
};
|
||||
|
||||
/* 删除单个 */
|
||||
const remove = (row: ShopSpec) => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeShopSpec(row.shopSpecId)
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
/* 批量删除 */
|
||||
const removeBatch = () => {
|
||||
if (!selection.value.length) {
|
||||
message.error('请至少选择一条数据');
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确定要删除选中的记录吗?',
|
||||
icon: createVNode(ExclamationCircleOutlined),
|
||||
maskClosable: true,
|
||||
onOk: () => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeBatchShopSpec(selection.value.map((d) => d.shopSpecId))
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/* 查询 */
|
||||
const query = () => {
|
||||
loading.value = true;
|
||||
};
|
||||
|
||||
/* 自定义行属性 */
|
||||
const customRow = (record: ShopSpec) => {
|
||||
return {
|
||||
// 行点击事件
|
||||
onClick: () => {
|
||||
// console.log(record);
|
||||
},
|
||||
// 行双击事件
|
||||
onDblclick: () => {
|
||||
openEdit(record);
|
||||
}
|
||||
};
|
||||
};
|
||||
query();
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ShopSpec'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
42
src/views/shop/shopSpecValue/components/search.vue
Normal file
42
src/views/shop/shopSpecValue/components/search.vue
Normal 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>
|
||||
197
src/views/shop/shopSpecValue/components/shopSpecValueEdit.vue
Normal file
197
src/views/shop/shopSpecValue/components/shopSpecValueEdit.vue
Normal file
@@ -0,0 +1,197 @@
|
||||
<!-- 编辑弹窗 -->
|
||||
<template>
|
||||
<ele-modal
|
||||
:width="800"
|
||||
:visible="visible"
|
||||
:maskClosable="false"
|
||||
:maxable="maxable"
|
||||
:title="isUpdate ? '编辑规格值' : '添加规格值'"
|
||||
:body-style="{ paddingBottom: '28px' }"
|
||||
@update:visible="updateVisible"
|
||||
@ok="save"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
|
||||
:wrapper-col="
|
||||
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
|
||||
"
|
||||
>
|
||||
<a-form-item label="规格组ID" name="specId">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入规格组ID"
|
||||
v-model:value="form.specId"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="规格值" name="specValue">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入规格值"
|
||||
v-model:value="form.specValue"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="备注" name="comments">
|
||||
<a-textarea
|
||||
:rows="4"
|
||||
:maxlength="200"
|
||||
placeholder="请输入描述"
|
||||
v-model:value="form.comments"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="排序号" name="sortNumber">
|
||||
<a-input-number
|
||||
:min="0"
|
||||
:max="9999"
|
||||
class="ele-fluid"
|
||||
placeholder="请输入排序号"
|
||||
v-model:value="form.sortNumber"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { Form, message } from 'ant-design-vue';
|
||||
import { assignObject, uuid } from 'ele-admin-pro';
|
||||
import {
|
||||
addShopSpecValue,
|
||||
updateShopSpecValue
|
||||
} from '@/api/shop/shopSpecValue';
|
||||
import { ShopSpecValue } from '@/api/shop/shopSpecValue/model';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
|
||||
import { FormInstance } from 'ant-design-vue/es/form';
|
||||
import { FileRecord } from '@/api/system/file/model';
|
||||
|
||||
// 是否是修改
|
||||
const isUpdate = ref(false);
|
||||
const useForm = Form.useForm;
|
||||
// 是否开启响应式布局
|
||||
const themeStore = useThemeStore();
|
||||
const { styleResponsive } = storeToRefs(themeStore);
|
||||
|
||||
const props = defineProps<{
|
||||
// 弹窗是否打开
|
||||
visible: boolean;
|
||||
// 修改回显的数据
|
||||
data?: ShopSpecValue | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
// 提交状态
|
||||
const loading = ref(false);
|
||||
// 是否显示最大化切换按钮
|
||||
const maxable = ref(true);
|
||||
// 表格选中数据
|
||||
const formRef = ref<FormInstance | null>(null);
|
||||
const images = ref<ItemType[]>([]);
|
||||
|
||||
// 用户信息
|
||||
const form = reactive<ShopSpecValue>({
|
||||
specValueId: undefined,
|
||||
specId: undefined,
|
||||
specValue: undefined,
|
||||
comments: undefined,
|
||||
tenantId: undefined,
|
||||
createTime: undefined,
|
||||
sortNumber: 100
|
||||
});
|
||||
|
||||
/* 更新visible */
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive({
|
||||
shopSpecValueName: [
|
||||
{
|
||||
required: true,
|
||||
type: 'string',
|
||||
message: '请填写规格值名称',
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const chooseImage = (data: FileRecord) => {
|
||||
images.value.push({
|
||||
uid: data.id,
|
||||
url: data.path,
|
||||
status: 'done'
|
||||
});
|
||||
form.image = data.path;
|
||||
};
|
||||
|
||||
const onDeleteItem = (index: number) => {
|
||||
images.value.splice(index, 1);
|
||||
form.image = '';
|
||||
};
|
||||
|
||||
const { resetFields } = useForm(form, rules);
|
||||
|
||||
/* 保存编辑 */
|
||||
const save = () => {
|
||||
if (!formRef.value) {
|
||||
return;
|
||||
}
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(() => {
|
||||
loading.value = true;
|
||||
const formData = {
|
||||
...form
|
||||
};
|
||||
const saveOrUpdate = isUpdate.value
|
||||
? updateShopSpecValue
|
||||
: addShopSpecValue;
|
||||
saveOrUpdate(formData)
|
||||
.then((msg) => {
|
||||
loading.value = false;
|
||||
message.success(msg);
|
||||
updateVisible(false);
|
||||
emit('done');
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
message.error(e.message);
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
images.value = [];
|
||||
if (props.data) {
|
||||
assignObject(form, props.data);
|
||||
if (props.data.image) {
|
||||
images.value.push({
|
||||
uid: uuid(),
|
||||
url: props.data.image,
|
||||
status: 'done'
|
||||
});
|
||||
}
|
||||
isUpdate.value = true;
|
||||
} else {
|
||||
isUpdate.value = false;
|
||||
}
|
||||
} else {
|
||||
resetFields();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
250
src/views/shop/shopSpecValue/index.vue
Normal file
250
src/views/shop/shopSpecValue/index.vue
Normal file
@@ -0,0 +1,250 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="ele-body">
|
||||
<a-card :bordered="false" :body-style="{ padding: '16px' }">
|
||||
<ele-pro-table
|
||||
ref="tableRef"
|
||||
row-key="shopSpecValueId"
|
||||
:columns="columns"
|
||||
:datasource="datasource"
|
||||
:customRow="customRow"
|
||||
tool-class="ele-toolbar-form"
|
||||
class="sys-org-table"
|
||||
>
|
||||
<template #toolbar>
|
||||
<search
|
||||
@search="reload"
|
||||
:selection="selection"
|
||||
@add="openEdit"
|
||||
@remove="removeBatch"
|
||||
@batchMove="openMove"
|
||||
/>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'image'">
|
||||
<a-image :src="record.image" :width="50" />
|
||||
</template>
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag v-if="record.status === 0" color="green">显示</a-tag>
|
||||
<a-tag v-if="record.status === 1" color="red">隐藏</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a @click="openEdit(record)">修改</a>
|
||||
<a-divider type="vertical" />
|
||||
<a-popconfirm
|
||||
title="确定要删除此记录吗?"
|
||||
@confirm="remove(record)"
|
||||
>
|
||||
<a class="ele-text-danger">删除</a>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<ShopSpecValueEdit
|
||||
v-model:visible="showEdit"
|
||||
:data="current"
|
||||
@done="reload"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { createVNode, ref } from 'vue';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
|
||||
import type { EleProTable } from 'ele-admin-pro';
|
||||
import { toDateString } from 'ele-admin-pro';
|
||||
import type {
|
||||
DatasourceFunction,
|
||||
ColumnItem
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import Search from './components/search.vue';
|
||||
import ShopSpecValueEdit from './components/shopSpecValueEdit.vue';
|
||||
import {
|
||||
pageShopSpecValue,
|
||||
removeShopSpecValue,
|
||||
removeBatchShopSpecValue
|
||||
} from '@/api/shop/shopSpecValue';
|
||||
import type {
|
||||
ShopSpecValue,
|
||||
ShopSpecValueParam
|
||||
} from '@/api/shop/shopSpecValue/model';
|
||||
|
||||
// 表格实例
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
|
||||
// 表格选中数据
|
||||
const selection = ref<ShopSpecValue[]>([]);
|
||||
// 当前编辑数据
|
||||
const current = ref<ShopSpecValue | null>(null);
|
||||
// 是否显示编辑弹窗
|
||||
const showEdit = ref(false);
|
||||
// 是否显示批量移动弹窗
|
||||
const showMove = ref(false);
|
||||
// 加载状态
|
||||
const loading = ref(true);
|
||||
|
||||
// 表格数据源
|
||||
const datasource: DatasourceFunction = ({
|
||||
page,
|
||||
limit,
|
||||
where,
|
||||
orders,
|
||||
filters
|
||||
}) => {
|
||||
if (filters) {
|
||||
where.status = filters.status;
|
||||
}
|
||||
return pageShopSpecValue({
|
||||
...where,
|
||||
...orders,
|
||||
page,
|
||||
limit
|
||||
});
|
||||
};
|
||||
|
||||
// 表格列配置
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
title: '规格值ID',
|
||||
dataIndex: 'specValueId',
|
||||
key: 'specValueId',
|
||||
align: 'center',
|
||||
width: 90
|
||||
},
|
||||
{
|
||||
title: '规格组ID',
|
||||
dataIndex: 'specId',
|
||||
key: 'specId',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '规格值',
|
||||
dataIndex: 'specValue',
|
||||
key: 'specValue',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
dataIndex: 'comments',
|
||||
key: 'comments',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '排序号',
|
||||
dataIndex: 'sortNumber',
|
||||
key: 'sortNumber',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
align: 'center',
|
||||
sorter: true,
|
||||
ellipsis: true,
|
||||
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd')
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 180,
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
hideInSetting: true
|
||||
}
|
||||
]);
|
||||
|
||||
/* 搜索 */
|
||||
const reload = (where?: ShopSpecValueParam) => {
|
||||
selection.value = [];
|
||||
tableRef?.value?.reload({ where: where });
|
||||
};
|
||||
|
||||
/* 打开编辑弹窗 */
|
||||
const openEdit = (row?: ShopSpecValue) => {
|
||||
current.value = row ?? null;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
/* 打开批量移动弹窗 */
|
||||
const openMove = () => {
|
||||
showMove.value = true;
|
||||
};
|
||||
|
||||
/* 删除单个 */
|
||||
const remove = (row: ShopSpecValue) => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeShopSpecValue(row.shopSpecValueId)
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
/* 批量删除 */
|
||||
const removeBatch = () => {
|
||||
if (!selection.value.length) {
|
||||
message.error('请至少选择一条数据');
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确定要删除选中的记录吗?',
|
||||
icon: createVNode(ExclamationCircleOutlined),
|
||||
maskClosable: true,
|
||||
onOk: () => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeBatchShopSpecValue(selection.value.map((d) => d.shopSpecValueId))
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/* 查询 */
|
||||
const query = () => {
|
||||
loading.value = true;
|
||||
};
|
||||
|
||||
/* 自定义行属性 */
|
||||
const customRow = (record: ShopSpecValue) => {
|
||||
return {
|
||||
// 行点击事件
|
||||
onClick: () => {
|
||||
// console.log(record);
|
||||
},
|
||||
// 行双击事件
|
||||
onDblclick: () => {
|
||||
openEdit(record);
|
||||
}
|
||||
};
|
||||
};
|
||||
query();
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ShopSpecValue'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
39
src/views/shop/shopUser/components/org-select.vue
Normal file
39
src/views/shop/shopUser/components/org-select.vue
Normal 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>
|
||||
42
src/views/shop/shopUser/components/search.vue
Normal file
42
src/views/shop/shopUser/components/search.vue
Normal 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>
|
||||
278
src/views/shop/shopUser/components/user-edit.vue
Normal file
278
src/views/shop/shopUser/components/user-edit.vue
Normal file
@@ -0,0 +1,278 @@
|
||||
<!-- 管理员编辑弹窗 -->
|
||||
<template>
|
||||
<ele-modal
|
||||
:width="500"
|
||||
:visible="visible"
|
||||
:confirm-loading="loading"
|
||||
:title="isUpdate ? '编辑客户' : '添加客户'"
|
||||
:body-style="{ paddingBottom: '8px' }"
|
||||
@update:visible="updateVisible"
|
||||
@ok="save"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
:label-col="styleResponsive ? { md: 5, sm: 4, xs: 24 } : { flex: '90px' }"
|
||||
:wrapper-col="
|
||||
styleResponsive ? { md: 17, sm: 20, xs: 24 } : { flex: '1' }
|
||||
"
|
||||
>
|
||||
<a-form-item label="姓名" name="realName">
|
||||
<a-input
|
||||
allow-clear
|
||||
:maxlength="20"
|
||||
placeholder="请输入真实姓名"
|
||||
v-model:value="form.realName"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="手机号" name="phone">
|
||||
<a-input
|
||||
allow-clear
|
||||
:maxlength="11"
|
||||
:disabled="isUpdate"
|
||||
placeholder="请输入手机号"
|
||||
v-model:value="form.phone"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="邮箱" name="email">
|
||||
<a-input
|
||||
allow-clear
|
||||
:maxlength="100"
|
||||
placeholder="请输入邮箱"
|
||||
v-model:value="form.email"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="所属机构" name="type">
|
||||
<org-select
|
||||
:data="organizationList"
|
||||
placeholder="请选择所属机构"
|
||||
v-model:value="form.organizationId"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { message } from 'ant-design-vue/es';
|
||||
import type { FormInstance, Rule } from 'ant-design-vue/es/form';
|
||||
import { emailReg, phoneReg } from 'ele-admin-pro/es';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import useFormData from '@/utils/use-form-data';
|
||||
import { addUser, updateUser, checkExistence } from '@/api/system/user';
|
||||
import type { User } from '@/api/system/user/model';
|
||||
import OrgSelect from './org-select.vue';
|
||||
import { Organization } from '@/api/system/organization/model';
|
||||
import { Grade } from '@/api/user/grade/model';
|
||||
import { TEMPLATE_ID } from '@/config/setting';
|
||||
|
||||
// 是否开启响应式布局
|
||||
const themeStore = useThemeStore();
|
||||
const { styleResponsive } = storeToRefs(themeStore);
|
||||
// 获取字典数据
|
||||
// const userTypeData = getDictionaryOptions('userType');
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps<{
|
||||
// 弹窗是否打开
|
||||
visible: boolean;
|
||||
// 修改回显的数据
|
||||
data?: User | null;
|
||||
// 全部机构
|
||||
organizationList: Organization[];
|
||||
}>();
|
||||
|
||||
//
|
||||
const formRef = ref<FormInstance | null>(null);
|
||||
|
||||
// 是否是修改
|
||||
const isUpdate = ref(false);
|
||||
|
||||
// 提交状态
|
||||
const loading = ref(false);
|
||||
|
||||
// 表单数据
|
||||
const { form, resetFields, assignFields } = useFormData<User>({
|
||||
type: undefined,
|
||||
userId: undefined,
|
||||
username: '',
|
||||
nickname: '',
|
||||
realName: '',
|
||||
companyName: '',
|
||||
sex: undefined,
|
||||
sexName: undefined,
|
||||
roles: [],
|
||||
email: '',
|
||||
phone: '',
|
||||
mobile: '',
|
||||
password: '',
|
||||
introduction: '',
|
||||
organizationId: undefined,
|
||||
birthday: '',
|
||||
idCard: '',
|
||||
comments: '',
|
||||
gradeName: '',
|
||||
isAdmin: true,
|
||||
gradeId: undefined,
|
||||
templateId: TEMPLATE_ID
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive<Record<string, Rule[]>>({
|
||||
username: [
|
||||
{
|
||||
required: true,
|
||||
type: 'string',
|
||||
validator: (_rule: Rule, value: string) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (!value) {
|
||||
return reject('请输入管理员账号');
|
||||
}
|
||||
checkExistence('username', value, props.data?.userId)
|
||||
.then(() => {
|
||||
reject('账号已经存在');
|
||||
})
|
||||
.catch(() => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
nickname: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入昵称',
|
||||
type: 'string',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
realName: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入真实姓名',
|
||||
type: 'string',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
// sex: [
|
||||
// {
|
||||
// required: true,
|
||||
// message: '请选择性别',
|
||||
// type: 'string',
|
||||
// trigger: 'blur'
|
||||
// }
|
||||
// ],
|
||||
roles: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择角色',
|
||||
type: 'array',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
email: [
|
||||
{
|
||||
pattern: emailReg,
|
||||
message: '邮箱格式不正确',
|
||||
type: 'string',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
password: [
|
||||
{
|
||||
required: true,
|
||||
type: 'string',
|
||||
validator: async (_rule: Rule, value: string) => {
|
||||
if (isUpdate.value || /^[\S]{5,18}$/.test(value)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject('密码必须为5-18位非空白字符');
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
phone: [
|
||||
{
|
||||
required: true,
|
||||
pattern: phoneReg,
|
||||
message: '手机号格式不正确',
|
||||
type: 'string',
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const chooseGradeId = (data: Grade) => {
|
||||
form.gradeName = data.name;
|
||||
form.gradeId = data.gradeId;
|
||||
};
|
||||
|
||||
const chooseSex = (data: any) => {
|
||||
form.sex = data.key;
|
||||
form.sexName = data.label;
|
||||
};
|
||||
|
||||
const updateIsAdmin = (value: boolean) => {
|
||||
form.isAdmin = value;
|
||||
};
|
||||
|
||||
/* 保存编辑 */
|
||||
const save = () => {
|
||||
if (!formRef.value) {
|
||||
return;
|
||||
}
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(() => {
|
||||
loading.value = true;
|
||||
const saveOrUpdate = isUpdate.value ? updateUser : addUser;
|
||||
form.username = form.phone;
|
||||
form.nickname = form.realName;
|
||||
saveOrUpdate(form)
|
||||
.then((msg) => {
|
||||
loading.value = false;
|
||||
message.success(msg);
|
||||
updateVisible(false);
|
||||
emit('done');
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
message.error(e.message);
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
/* 更新visible */
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
if (props.data) {
|
||||
assignFields({
|
||||
...props.data,
|
||||
password: ''
|
||||
});
|
||||
isUpdate.value = true;
|
||||
} else {
|
||||
isUpdate.value = false;
|
||||
}
|
||||
} else {
|
||||
resetFields();
|
||||
formRef.value?.clearValidate();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
88
src/views/shop/shopUser/components/user-import.vue
Normal file
88
src/views/shop/shopUser/components/user-import.vue
Normal 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>只能上传xls、xlsx文件,</span>
|
||||
<a
|
||||
href="https://server.websoft.top/api/system/user/import/template"
|
||||
download="用户导入模板.xlsx"
|
||||
>
|
||||
下载导入模板
|
||||
</a>
|
||||
</div>
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { message } from 'ant-design-vue/es';
|
||||
import { CloudUploadOutlined } from '@ant-design/icons-vue';
|
||||
import { importUsers } from '@/api/system/user';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
defineProps<{
|
||||
// 是否打开弹窗
|
||||
visible: boolean;
|
||||
}>();
|
||||
|
||||
// 导入请求状态
|
||||
const loading = ref(false);
|
||||
|
||||
/* 上传 */
|
||||
const doUpload = ({ file }) => {
|
||||
if (
|
||||
![
|
||||
'application/vnd.ms-excel',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
].includes(file.type)
|
||||
) {
|
||||
message.error('只能选择 excel 文件');
|
||||
return false;
|
||||
}
|
||||
if (file.size / 1024 / 1024 > 10) {
|
||||
message.error('大小不能超过 10MB');
|
||||
return false;
|
||||
}
|
||||
loading.value = true;
|
||||
importUsers(file)
|
||||
.then((msg) => {
|
||||
loading.value = false;
|
||||
message.success(msg);
|
||||
updateVisible(false);
|
||||
emit('done');
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
message.error(e.message);
|
||||
});
|
||||
return false;
|
||||
};
|
||||
|
||||
/* 更新 visible */
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
</script>
|
||||
606
src/views/shop/shopUser/index.vue
Normal file
606
src/views/shop/shopUser/index.vue
Normal file
@@ -0,0 +1,606 @@
|
||||
<template>
|
||||
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
|
||||
<a-card :bordered="false">
|
||||
<!-- 表格 -->
|
||||
<ele-pro-table
|
||||
ref="tableRef"
|
||||
row-key="userId"
|
||||
:columns="columns"
|
||||
:datasource="datasource"
|
||||
class="sys-org-table"
|
||||
:scroll="{ x: 1300 }"
|
||||
:where="defaultWhere"
|
||||
:customRow="customRow"
|
||||
cache-key="proSystemUserTable"
|
||||
>
|
||||
<template #toolbar>
|
||||
<a-space>
|
||||
<a-button type="primary" class="ele-btn-icon" @click="openEdit()">
|
||||
<template #icon>
|
||||
<plus-outlined />
|
||||
</template>
|
||||
<span>添加</span>
|
||||
</a-button>
|
||||
<a-button class="ele-btn-icon" @click="openImport()">
|
||||
<template #icon>
|
||||
<cloud-upload-outlined />
|
||||
</template>
|
||||
<span>导入</span>
|
||||
</a-button>
|
||||
<a-button
|
||||
class="ele-btn-icon"
|
||||
@click="exportData()"
|
||||
:loading="exportLoading"
|
||||
>
|
||||
<template #icon>
|
||||
<download-outlined />
|
||||
</template>
|
||||
<span>导出</span>
|
||||
</a-button>
|
||||
<a-input-search
|
||||
allow-clear
|
||||
v-model:value="searchText"
|
||||
placeholder="请输入关键词"
|
||||
@search="reload"
|
||||
@pressEnter="reload"
|
||||
/>
|
||||
</a-space>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'avatar'">
|
||||
<a-avatar
|
||||
:size="30"
|
||||
:src="`${record.avatar}`"
|
||||
style="margin-right: 4px"
|
||||
>
|
||||
<template #icon>
|
||||
<UserOutlined />
|
||||
</template>
|
||||
</a-avatar>
|
||||
</template>
|
||||
<template v-if="column.key === 'nickname'">
|
||||
<div>{{ record.nickname }}</div>
|
||||
<div class="text-gray-400">{{ record.realName }}</div>
|
||||
</template>
|
||||
<template v-if="column.key === 'phone'">
|
||||
<span v-if="hasRole('superAdmin')">{{ record.phone }}</span>
|
||||
<span v-else>{{ record.phone }}</span>
|
||||
</template>
|
||||
<template v-if="column.key === 'roles'">
|
||||
<a-tag v-for="item in record.roles" :key="item.roleId" color="blue">
|
||||
{{ item.roleName }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'platform'">
|
||||
<WechatOutlined v-if="record.platform === 'MP-WEIXIN'" />
|
||||
<Html5Outlined v-if="record.platform === 'H5'" />
|
||||
<ChromeOutlined v-if="record.platform === 'WEB'" />
|
||||
</template>
|
||||
<template v-if="column.key === 'balance'">
|
||||
<span class="ele-text-success">
|
||||
¥{{ formatNumber(record.balance) }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-if="column.key === 'expendMoney'">
|
||||
<span class="ele-text-warning">
|
||||
¥{{ formatNumber(record.expendMoney) }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-if="column.key === 'isAdmin'">
|
||||
<a-switch
|
||||
:checked="record.isAdmin == 1"
|
||||
@change="updateIsAdmin(record)"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<div>
|
||||
<a @click="openEdit(record)">修改</a>
|
||||
<a-divider type="vertical" />
|
||||
<a-popconfirm
|
||||
placement="topRight"
|
||||
title="确定要删除此用户吗?"
|
||||
@confirm="remove(record)"
|
||||
>
|
||||
<a class="ele-text-danger">删除</a>
|
||||
</a-popconfirm>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
</a-card>
|
||||
<!-- 编辑弹窗 -->
|
||||
<user-edit
|
||||
v-model:visible="showEdit"
|
||||
:data="current"
|
||||
:organization-list="data"
|
||||
@done="reload"
|
||||
/>
|
||||
<!-- 导入弹窗 -->
|
||||
<user-import v-model:visible="showImport" @done="reload" />
|
||||
</a-page-header>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { createVNode, ref, reactive, watch } from 'vue';
|
||||
import { message, Modal } from 'ant-design-vue/es';
|
||||
import {
|
||||
PlusOutlined,
|
||||
UserOutlined,
|
||||
Html5Outlined,
|
||||
ChromeOutlined,
|
||||
WechatOutlined,
|
||||
CloudUploadOutlined,
|
||||
DownloadOutlined,
|
||||
ExclamationCircleOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
import type { EleProTable } from 'ele-admin-pro/es';
|
||||
import type {
|
||||
DatasourceFunction,
|
||||
ColumnItem
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import { messageLoading, formatNumber } from 'ele-admin-pro/es';
|
||||
import UserEdit from './components/user-edit.vue';
|
||||
import UserImport from './components/user-import.vue';
|
||||
import { toDateString } from 'ele-admin-pro';
|
||||
import { utils, writeFile } from 'xlsx';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
pageShopUser,
|
||||
removeShopUser,
|
||||
removeBatchShopUser,
|
||||
updateShopUser,
|
||||
listShopUser
|
||||
} from '@/api/shop/shopUser';
|
||||
import type { ShopUser, ShopUserParam } from '@/api/shop/shopUser/model';
|
||||
import { toTreeData, uuid } from 'ele-admin-pro';
|
||||
import { listRoles } from '@/api/system/role';
|
||||
import { listOrganizations } from '@/api/system/organization';
|
||||
import { Organization } from '@/api/system/organization/model';
|
||||
import { hasRole } from '@/utils/permission';
|
||||
import { getPageTitle } from '@/utils/common';
|
||||
import router from '@/router';
|
||||
import { getTenantId } from '@/utils/domain';
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(true);
|
||||
// 树形数据
|
||||
const data = ref<Organization[]>([]);
|
||||
// 树展开的key
|
||||
const expandedRowKeys = ref<number[]>([]);
|
||||
// 树选中的key
|
||||
const selectedRowKeys = ref<number[]>([]);
|
||||
// 表格选中数据
|
||||
const selection = ref<ShopUser[]>([]);
|
||||
// 当前编辑数据
|
||||
const current = ref<ShopUser | null>(null);
|
||||
// 是否显示编辑弹窗
|
||||
const showEdit = ref(false);
|
||||
// 是否显示用户详情
|
||||
const showInfo = ref(false);
|
||||
// 是否显示用户导入弹窗
|
||||
const showImport = ref(false);
|
||||
// 导出加载状态
|
||||
const exportLoading = ref(false);
|
||||
const userType = ref<number>();
|
||||
const searchText = ref('');
|
||||
|
||||
// 加载角色
|
||||
const roles = ref<any[]>([]);
|
||||
// 加载机构
|
||||
listOrganizations()
|
||||
.then((list) => {
|
||||
loading.value = false;
|
||||
const eks: number[] = [];
|
||||
list.forEach((d) => {
|
||||
d.key = d.organizationId;
|
||||
d.value = d.organizationId;
|
||||
d.title = d.organizationName;
|
||||
if (typeof d.key === 'number') {
|
||||
eks.push(d.key);
|
||||
}
|
||||
});
|
||||
expandedRowKeys.value = eks;
|
||||
data.value = toTreeData({
|
||||
data: list,
|
||||
idField: 'organizationId',
|
||||
parentIdField: 'parentId'
|
||||
});
|
||||
if (list.length) {
|
||||
if (typeof list[0].key === 'number') {
|
||||
selectedRowKeys.value = [list[0].key];
|
||||
}
|
||||
// current.value = list[0];
|
||||
} else {
|
||||
selectedRowKeys.value = [];
|
||||
// current.value = null;
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
message.error(e.message);
|
||||
});
|
||||
|
||||
// 表格实例
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
// 表格列配置
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'userId',
|
||||
width: 90,
|
||||
showSorterTooltip: false
|
||||
},
|
||||
{
|
||||
title: '昵称/姓名',
|
||||
dataIndex: 'nickname',
|
||||
key: 'nickname',
|
||||
align: 'center',
|
||||
showSorterTooltip: false
|
||||
},
|
||||
{
|
||||
title: '手机号码',
|
||||
dataIndex: 'phone',
|
||||
align: 'center',
|
||||
showSorterTooltip: false
|
||||
},
|
||||
{
|
||||
title: '积分',
|
||||
dataIndex: 'points',
|
||||
align: 'center',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '余额',
|
||||
dataIndex: 'balance',
|
||||
align: 'center',
|
||||
width: 100
|
||||
},
|
||||
// {
|
||||
// title: '角色',
|
||||
// dataIndex: 'roles',
|
||||
// key: 'roles',
|
||||
// align: 'center'
|
||||
// },
|
||||
{
|
||||
title: '备注',
|
||||
dataIndex: 'comments',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
align: 'center',
|
||||
sorter: true,
|
||||
customRender: ({ text }) => {
|
||||
return text === 1
|
||||
? createVNode(
|
||||
'span',
|
||||
{
|
||||
class: 'text-red-400'
|
||||
},
|
||||
'封号'
|
||||
)
|
||||
: createVNode(
|
||||
'span',
|
||||
{
|
||||
class: 'text-gray-400'
|
||||
},
|
||||
'正常'
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
sorter: true,
|
||||
align: 'center',
|
||||
showSorterTooltip: false,
|
||||
ellipsis: true,
|
||||
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 180,
|
||||
fixed: 'right',
|
||||
align: 'center'
|
||||
}
|
||||
]);
|
||||
|
||||
// 默认搜索条件
|
||||
const defaultWhere = reactive({
|
||||
username: '',
|
||||
nickname: ''
|
||||
});
|
||||
|
||||
// 表格数据源
|
||||
const datasource: DatasourceFunction = ({
|
||||
page,
|
||||
limit,
|
||||
where,
|
||||
orders,
|
||||
filters
|
||||
}) => {
|
||||
where = {};
|
||||
where.roleId = filters.roles;
|
||||
where.keywords = searchText.value;
|
||||
return pageShopUser({ page, limit, ...where, ...orders });
|
||||
};
|
||||
|
||||
/* 搜索 */
|
||||
const reload = (where?: ShopUserParam) => {
|
||||
selection.value = [];
|
||||
tableRef?.value?.reload({ where });
|
||||
};
|
||||
|
||||
/* 打开编辑弹窗 */
|
||||
const openEdit = (row?: ShopUser) => {
|
||||
current.value = row ?? null;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
/* 打开用户详情弹窗 */
|
||||
const openInfo = (row?: ShopUser) => {
|
||||
current.value = row ?? null;
|
||||
showInfo.value = true;
|
||||
};
|
||||
|
||||
/* 打开编辑弹窗 */
|
||||
const openImport = () => {
|
||||
showImport.value = true;
|
||||
};
|
||||
|
||||
/* 导出数据 */
|
||||
const exportData = async () => {
|
||||
exportLoading.value = true;
|
||||
|
||||
try {
|
||||
// 定义表头
|
||||
const array: (string | number)[][] = [
|
||||
[
|
||||
'用户ID',
|
||||
'账号',
|
||||
'昵称',
|
||||
'真实姓名',
|
||||
'手机号',
|
||||
'邮箱',
|
||||
'性别',
|
||||
'状态',
|
||||
'注册时间'
|
||||
]
|
||||
];
|
||||
|
||||
// 构建查询参数,使用当前搜索条件
|
||||
const params = {
|
||||
keywords: searchText.value,
|
||||
isAdmin: 0
|
||||
};
|
||||
|
||||
// 获取用户列表数据
|
||||
const list = await listShopUser(params);
|
||||
|
||||
if (!list || list.length === 0) {
|
||||
message.warning('没有数据可以导出');
|
||||
exportLoading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 将数据转换为Excel行
|
||||
list.forEach((user: ShopUser) => {
|
||||
array.push([
|
||||
`${user.userId || ''}`,
|
||||
`${user.username || ''}`,
|
||||
`${user.nickname || ''}`,
|
||||
`${user.realName || ''}`,
|
||||
`${user.phone || ''}`,
|
||||
`${user.email || ''}`,
|
||||
`${user.sex == 1 ? '男' : '女'}`,
|
||||
`${user.status === 0 ? '正常' : '冻结'}`,
|
||||
`${user.createTime || ''}`
|
||||
]);
|
||||
});
|
||||
|
||||
// 生成Excel文件
|
||||
const sheetName = `shop_user_${getTenantId()}_${dayjs(new Date()).format(
|
||||
'YYYYMMDD'
|
||||
)}`;
|
||||
const workbook = {
|
||||
SheetNames: [sheetName],
|
||||
Sheets: {}
|
||||
};
|
||||
const sheet = utils.aoa_to_sheet(array);
|
||||
workbook.Sheets[sheetName] = sheet;
|
||||
|
||||
// 设置列宽
|
||||
sheet['!cols'] = [
|
||||
{ wch: 10 }, // 用户ID
|
||||
{ wch: 15 }, // 账号
|
||||
{ wch: 12 }, // 昵称
|
||||
{ wch: 12 }, // 真实姓名
|
||||
{ wch: 15 }, // 手机号
|
||||
{ wch: 20 }, // 邮箱
|
||||
{ wch: 8 }, // 性别
|
||||
{ wch: 15 }, // 所属部门
|
||||
{ wch: 20 }, // 角色
|
||||
{ wch: 8 }, // 状态
|
||||
{ wch: 20 } // 注册时间
|
||||
];
|
||||
|
||||
message.loading('正在生成Excel文件...', 0);
|
||||
|
||||
setTimeout(() => {
|
||||
writeFile(workbook, `${sheetName}.xlsx`);
|
||||
exportLoading.value = false;
|
||||
message.destroy();
|
||||
message.success(`成功导出 ${list.length} 条记录`);
|
||||
}, 1000);
|
||||
} catch (error: any) {
|
||||
exportLoading.value = false;
|
||||
message.error(error.message || '导出失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleTabs = (e) => {
|
||||
userType.value = Number(e.target.value);
|
||||
reload();
|
||||
};
|
||||
|
||||
/* 删除单个 */
|
||||
const remove = (row: ShopUser) => {
|
||||
const hide = messageLoading('请求中..', 0);
|
||||
removeShopUser(row.userId)
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
/* 批量删除 */
|
||||
const removeBatch = () => {
|
||||
if (!selection.value.length) {
|
||||
message.error('请至少选择一条数据');
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确定要删除选中的用户吗?',
|
||||
icon: createVNode(ExclamationCircleOutlined),
|
||||
maskClosable: true,
|
||||
onOk: () => {
|
||||
const hide = messageLoading('请求中..', 0);
|
||||
removeShopUser(selection.value.map((d) => d.userId))
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/* 重置用户密码 */
|
||||
const resetPsw = (row: ShopUser) => {
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确定要重置此用户的密码吗?',
|
||||
icon: createVNode(ExclamationCircleOutlined),
|
||||
maskClosable: true,
|
||||
onOk: () => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
const password = uuid(8);
|
||||
updateShopUser({
|
||||
...row,
|
||||
password: password
|
||||
})
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg + ',新密码:' + password);
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/* 修改用户状态 */
|
||||
const updateIsAdmin = (row: ShopUser) => {
|
||||
updateShopUser(row)
|
||||
.then((msg) => {
|
||||
message.success(msg);
|
||||
})
|
||||
.catch((e) => {
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
/* 自定义行属性 */
|
||||
const customRow = (record: ShopUser) => {
|
||||
return {
|
||||
// 行点击事件
|
||||
onClick: () => {
|
||||
// console.log(record);
|
||||
},
|
||||
// 行双击事件
|
||||
onDblclick: () => {
|
||||
openEdit(record);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const query = async () => {
|
||||
const info = await listRoles({});
|
||||
if (info) {
|
||||
roles.value = info;
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => router.currentRoute.value.query,
|
||||
() => {
|
||||
query();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ShopUser'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.sys-org-table {
|
||||
:deep(.ant-table) {
|
||||
.ant-table-thead > tr > th {
|
||||
background: #fafafa;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr > td {
|
||||
padding: 12px 8px;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr:hover > td {
|
||||
background: #f8f9ff;
|
||||
}
|
||||
|
||||
.ant-tag {
|
||||
margin: 0;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
padding: 2px 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ele-text-primary {
|
||||
color: #1890ff;
|
||||
|
||||
&:hover {
|
||||
color: #40a9ff;
|
||||
}
|
||||
}
|
||||
|
||||
.ele-text-danger {
|
||||
color: #ff4d4f;
|
||||
|
||||
&:hover {
|
||||
color: #ff7875;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
42
src/views/shop/shopUserCoupon/components/search.vue
Normal file
42
src/views/shop/shopUserCoupon/components/search.vue
Normal 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>
|
||||
327
src/views/shop/shopUserCoupon/components/shopUserCouponEdit.vue
Normal file
327
src/views/shop/shopUserCoupon/components/shopUserCouponEdit.vue
Normal file
@@ -0,0 +1,327 @@
|
||||
<!-- 编辑弹窗 -->
|
||||
<template>
|
||||
<ele-modal
|
||||
:width="800"
|
||||
:visible="visible"
|
||||
:maskClosable="false"
|
||||
:maxable="maxable"
|
||||
:title="isUpdate ? '编辑用户优惠券' : '添加用户优惠券'"
|
||||
:body-style="{ paddingBottom: '28px' }"
|
||||
@update:visible="updateVisible"
|
||||
@ok="save"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
|
||||
:wrapper-col="
|
||||
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
|
||||
"
|
||||
>
|
||||
<a-form-item label="优惠券模板ID" name="couponId">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入优惠券模板ID"
|
||||
v-model:value="form.couponId"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="用户ID" name="userId">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入用户ID"
|
||||
v-model:value="form.userId"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="优惠券名称" name="name">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入优惠券名称"
|
||||
v-model:value="form.name"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="优惠券描述" name="description">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入优惠券描述"
|
||||
v-model:value="form.description"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="优惠券类型(10满减券 20折扣券 30免费劵)" name="type">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入优惠券类型(10满减券 20折扣券 30免费劵)"
|
||||
v-model:value="form.type"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="满减券-减免金额" name="reducePrice">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入满减券-减免金额"
|
||||
v-model:value="form.reducePrice"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="折扣券-折扣率(0-100)" name="discount">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入折扣券-折扣率(0-100)"
|
||||
v-model:value="form.discount"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="最低消费金额" name="minPrice">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入最低消费金额"
|
||||
v-model:value="form.minPrice"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
label="适用范围(10全部商品 20指定商品 30指定分类)"
|
||||
name="applyRange"
|
||||
>
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入适用范围(10全部商品 20指定商品 30指定分类)"
|
||||
v-model:value="form.applyRange"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="适用范围配置(json格式)" name="applyRangeConfig">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入适用范围配置(json格式)"
|
||||
v-model:value="form.applyRangeConfig"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="有效期开始时间" name="startTime">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入有效期开始时间"
|
||||
v-model:value="form.startTime"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="有效期结束时间" name="endTime">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入有效期结束时间"
|
||||
v-model:value="form.endTime"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="使用状态(0未使用 1已使用 2已过期)" name="status">
|
||||
<a-radio-group v-model:value="form.status">
|
||||
<a-radio :value="0">显示</a-radio>
|
||||
<a-radio :value="1">隐藏</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="使用时间" name="useTime">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入使用时间"
|
||||
v-model:value="form.useTime"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="使用订单ID" name="orderId">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入使用订单ID"
|
||||
v-model:value="form.orderId"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="使用订单号" name="orderNo">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入使用订单号"
|
||||
v-model:value="form.orderNo"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
label="获取方式(10主动领取 20系统发放 30活动赠送)"
|
||||
name="obtainType"
|
||||
>
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入获取方式(10主动领取 20系统发放 30活动赠送)"
|
||||
v-model:value="form.obtainType"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="获取来源描述" name="obtainSource">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入获取来源描述"
|
||||
v-model:value="form.obtainSource"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="是否删除, 0否, 1是" name="deleted">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入是否删除, 0否, 1是"
|
||||
v-model:value="form.deleted"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="修改时间" name="updateTime">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入修改时间"
|
||||
v-model:value="form.updateTime"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { Form, message } from 'ant-design-vue';
|
||||
import { assignObject, uuid } from 'ele-admin-pro';
|
||||
import {
|
||||
addShopUserCoupon,
|
||||
updateShopUserCoupon
|
||||
} from '@/api/shop/shopUserCoupon';
|
||||
import { ShopUserCoupon } from '@/api/shop/shopUserCoupon/model';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
|
||||
import { FormInstance } from 'ant-design-vue/es/form';
|
||||
import { FileRecord } from '@/api/system/file/model';
|
||||
|
||||
// 是否是修改
|
||||
const isUpdate = ref(false);
|
||||
const useForm = Form.useForm;
|
||||
// 是否开启响应式布局
|
||||
const themeStore = useThemeStore();
|
||||
const { styleResponsive } = storeToRefs(themeStore);
|
||||
|
||||
const props = defineProps<{
|
||||
// 弹窗是否打开
|
||||
visible: boolean;
|
||||
// 修改回显的数据
|
||||
data?: ShopUserCoupon | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
// 提交状态
|
||||
const loading = ref(false);
|
||||
// 是否显示最大化切换按钮
|
||||
const maxable = ref(true);
|
||||
// 表格选中数据
|
||||
const formRef = ref<FormInstance | null>(null);
|
||||
const images = ref<ItemType[]>([]);
|
||||
|
||||
// 用户信息
|
||||
const form = reactive<ShopUserCoupon>({
|
||||
id: undefined,
|
||||
couponId: undefined,
|
||||
userId: undefined,
|
||||
name: undefined,
|
||||
description: undefined,
|
||||
type: undefined,
|
||||
reducePrice: undefined,
|
||||
discount: undefined,
|
||||
minPrice: undefined,
|
||||
applyRange: undefined,
|
||||
applyRangeConfig: undefined,
|
||||
startTime: undefined,
|
||||
endTime: undefined,
|
||||
status: undefined,
|
||||
useTime: undefined,
|
||||
orderId: undefined,
|
||||
orderNo: undefined,
|
||||
obtainType: undefined,
|
||||
obtainSource: undefined,
|
||||
deleted: undefined,
|
||||
tenantId: undefined,
|
||||
createTime: undefined,
|
||||
updateTime: undefined
|
||||
});
|
||||
|
||||
/* 更新visible */
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive({
|
||||
shopUserCouponName: [
|
||||
{
|
||||
required: true,
|
||||
type: 'string',
|
||||
message: '请填写用户优惠券名称',
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const chooseImage = (data: FileRecord) => {
|
||||
images.value.push({
|
||||
uid: data.id,
|
||||
url: data.path,
|
||||
status: 'done'
|
||||
});
|
||||
form.image = data.path;
|
||||
};
|
||||
|
||||
const onDeleteItem = (index: number) => {
|
||||
images.value.splice(index, 1);
|
||||
form.image = '';
|
||||
};
|
||||
|
||||
const { resetFields } = useForm(form, rules);
|
||||
|
||||
/* 保存编辑 */
|
||||
const save = () => {
|
||||
if (!formRef.value) {
|
||||
return;
|
||||
}
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(() => {
|
||||
loading.value = true;
|
||||
const formData = {
|
||||
...form
|
||||
};
|
||||
const saveOrUpdate = isUpdate.value
|
||||
? updateShopUserCoupon
|
||||
: addShopUserCoupon;
|
||||
saveOrUpdate(formData)
|
||||
.then((msg) => {
|
||||
loading.value = false;
|
||||
message.success(msg);
|
||||
updateVisible(false);
|
||||
emit('done');
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
message.error(e.message);
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
images.value = [];
|
||||
if (props.data) {
|
||||
assignObject(form, props.data);
|
||||
if (props.data.image) {
|
||||
images.value.push({
|
||||
uid: uuid(),
|
||||
url: props.data.image,
|
||||
status: 'done'
|
||||
});
|
||||
}
|
||||
isUpdate.value = true;
|
||||
} else {
|
||||
isUpdate.value = false;
|
||||
}
|
||||
} else {
|
||||
resetFields();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
347
src/views/shop/shopUserCoupon/index.vue
Normal file
347
src/views/shop/shopUserCoupon/index.vue
Normal file
@@ -0,0 +1,347 @@
|
||||
<template>
|
||||
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
|
||||
<a-card :bordered="false" :body-style="{ padding: '16px' }">
|
||||
<ele-pro-table
|
||||
ref="tableRef"
|
||||
row-key="shopUserCouponId"
|
||||
:columns="columns"
|
||||
:datasource="datasource"
|
||||
:customRow="customRow"
|
||||
tool-class="ele-toolbar-form"
|
||||
class="sys-org-table"
|
||||
>
|
||||
<template #toolbar>
|
||||
<search
|
||||
@search="reload"
|
||||
:selection="selection"
|
||||
@add="openEdit"
|
||||
@remove="removeBatch"
|
||||
@batchMove="openMove"
|
||||
/>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'image'">
|
||||
<a-image :src="record.image" :width="50" />
|
||||
</template>
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag v-if="record.status === 0" color="green">显示</a-tag>
|
||||
<a-tag v-if="record.status === 1" color="red">隐藏</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a @click="openEdit(record)">修改</a>
|
||||
<a-divider type="vertical" />
|
||||
<a-popconfirm
|
||||
title="确定要删除此记录吗?"
|
||||
@confirm="remove(record)"
|
||||
>
|
||||
<a class="ele-text-danger">删除</a>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<ShopUserCouponEdit
|
||||
v-model:visible="showEdit"
|
||||
:data="current"
|
||||
@done="reload"
|
||||
/>
|
||||
</a-page-header>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { createVNode, ref } from 'vue';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
|
||||
import type { EleProTable } from 'ele-admin-pro';
|
||||
import { toDateString } from 'ele-admin-pro';
|
||||
import type {
|
||||
DatasourceFunction,
|
||||
ColumnItem
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import Search from './components/search.vue';
|
||||
import { getPageTitle } from '@/utils/common';
|
||||
import ShopUserCouponEdit from './components/shopUserCouponEdit.vue';
|
||||
import {
|
||||
pageShopUserCoupon,
|
||||
removeShopUserCoupon,
|
||||
removeBatchShopUserCoupon
|
||||
} from '@/api/shop/shopUserCoupon';
|
||||
import type {
|
||||
ShopUserCoupon,
|
||||
ShopUserCouponParam
|
||||
} from '@/api/shop/shopUserCoupon/model';
|
||||
|
||||
// 表格实例
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
|
||||
// 表格选中数据
|
||||
const selection = ref<ShopUserCoupon[]>([]);
|
||||
// 当前编辑数据
|
||||
const current = ref<ShopUserCoupon | null>(null);
|
||||
// 是否显示编辑弹窗
|
||||
const showEdit = ref(false);
|
||||
// 是否显示批量移动弹窗
|
||||
const showMove = ref(false);
|
||||
// 加载状态
|
||||
const loading = ref(true);
|
||||
|
||||
// 表格数据源
|
||||
const datasource: DatasourceFunction = ({
|
||||
page,
|
||||
limit,
|
||||
where,
|
||||
orders,
|
||||
filters
|
||||
}) => {
|
||||
if (filters) {
|
||||
where.status = filters.status;
|
||||
}
|
||||
return pageShopUserCoupon({
|
||||
...where,
|
||||
...orders,
|
||||
page,
|
||||
limit
|
||||
});
|
||||
};
|
||||
|
||||
// 表格列配置
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
title: 'id',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
align: 'center',
|
||||
width: 90
|
||||
},
|
||||
{
|
||||
title: '优惠券模板ID',
|
||||
dataIndex: 'couponId',
|
||||
key: 'couponId',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '用户ID',
|
||||
dataIndex: 'userId',
|
||||
key: 'userId',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '优惠券名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '优惠券描述',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '优惠券类型(10满减券 20折扣券 30免费劵)',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '满减券-减免金额',
|
||||
dataIndex: 'reducePrice',
|
||||
key: 'reducePrice',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '折扣券-折扣率(0-100)',
|
||||
dataIndex: 'discount',
|
||||
key: 'discount',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '最低消费金额',
|
||||
dataIndex: 'minPrice',
|
||||
key: 'minPrice',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '适用范围(10全部商品 20指定商品 30指定分类)',
|
||||
dataIndex: 'applyRange',
|
||||
key: 'applyRange',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '适用范围配置(json格式)',
|
||||
dataIndex: 'applyRangeConfig',
|
||||
key: 'applyRangeConfig',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '有效期开始时间',
|
||||
dataIndex: 'startTime',
|
||||
key: 'startTime',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '有效期结束时间',
|
||||
dataIndex: 'endTime',
|
||||
key: 'endTime',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '使用状态(0未使用 1已使用 2已过期)',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '使用时间',
|
||||
dataIndex: 'useTime',
|
||||
key: 'useTime',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '使用订单ID',
|
||||
dataIndex: 'orderId',
|
||||
key: 'orderId',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '使用订单号',
|
||||
dataIndex: 'orderNo',
|
||||
key: 'orderNo',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '获取方式(10主动领取 20系统发放 30活动赠送)',
|
||||
dataIndex: 'obtainType',
|
||||
key: 'obtainType',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '获取来源描述',
|
||||
dataIndex: 'obtainSource',
|
||||
key: 'obtainSource',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '是否删除, 0否, 1是',
|
||||
dataIndex: 'deleted',
|
||||
key: 'deleted',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
align: 'center',
|
||||
sorter: true,
|
||||
ellipsis: true,
|
||||
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd')
|
||||
},
|
||||
{
|
||||
title: '修改时间',
|
||||
dataIndex: 'updateTime',
|
||||
key: 'updateTime',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 180,
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
hideInSetting: true
|
||||
}
|
||||
]);
|
||||
|
||||
/* 搜索 */
|
||||
const reload = (where?: ShopUserCouponParam) => {
|
||||
selection.value = [];
|
||||
tableRef?.value?.reload({ where: where });
|
||||
};
|
||||
|
||||
/* 打开编辑弹窗 */
|
||||
const openEdit = (row?: ShopUserCoupon) => {
|
||||
current.value = row ?? null;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
/* 打开批量移动弹窗 */
|
||||
const openMove = () => {
|
||||
showMove.value = true;
|
||||
};
|
||||
|
||||
/* 删除单个 */
|
||||
const remove = (row: ShopUserCoupon) => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeShopUserCoupon(row.shopUserCouponId)
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
/* 批量删除 */
|
||||
const removeBatch = () => {
|
||||
if (!selection.value.length) {
|
||||
message.error('请至少选择一条数据');
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确定要删除选中的记录吗?',
|
||||
icon: createVNode(ExclamationCircleOutlined),
|
||||
maskClosable: true,
|
||||
onOk: () => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeBatchShopUserCoupon(
|
||||
selection.value.map((d) => d.shopUserCouponId)
|
||||
)
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/* 查询 */
|
||||
const query = () => {
|
||||
loading.value = true;
|
||||
};
|
||||
|
||||
/* 自定义行属性 */
|
||||
const customRow = (record: ShopUserCoupon) => {
|
||||
return {
|
||||
// 行点击事件
|
||||
onClick: () => {
|
||||
// console.log(record);
|
||||
},
|
||||
// 行双击事件
|
||||
onDblclick: () => {
|
||||
openEdit(record);
|
||||
}
|
||||
};
|
||||
};
|
||||
query();
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ShopUserCoupon'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
42
src/views/shop/shopUserReferee/components/search.vue
Normal file
42
src/views/shop/shopUserReferee/components/search.vue
Normal 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>
|
||||
@@ -0,0 +1,211 @@
|
||||
<!-- 编辑弹窗 -->
|
||||
<template>
|
||||
<ele-modal
|
||||
:width="800"
|
||||
:visible="visible"
|
||||
:maskClosable="false"
|
||||
:maxable="maxable"
|
||||
:title="isUpdate ? '编辑用户推荐关系表' : '添加用户推荐关系表'"
|
||||
:body-style="{ paddingBottom: '28px' }"
|
||||
@update:visible="updateVisible"
|
||||
@ok="save"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
|
||||
:wrapper-col="
|
||||
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
|
||||
"
|
||||
>
|
||||
<a-form-item label="推荐人ID" name="dealerId">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入推荐人ID"
|
||||
v-model:value="form.dealerId"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="用户id(被推荐人)" name="userId">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入用户id(被推荐人)"
|
||||
v-model:value="form.userId"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="推荐关系层级(1,2,3)" name="level">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入推荐关系层级(1,2,3)"
|
||||
v-model:value="form.level"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="备注" name="comments">
|
||||
<a-textarea
|
||||
:rows="4"
|
||||
:maxlength="200"
|
||||
placeholder="请输入描述"
|
||||
v-model:value="form.comments"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="是否删除, 0否, 1是" name="deleted">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入是否删除, 0否, 1是"
|
||||
v-model:value="form.deleted"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="修改时间" name="updateTime">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入修改时间"
|
||||
v-model:value="form.updateTime"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { Form, message } from 'ant-design-vue';
|
||||
import { assignObject, uuid } from 'ele-admin-pro';
|
||||
import {
|
||||
addShopUserReferee,
|
||||
updateShopUserReferee
|
||||
} from '@/api/shop/shopUserReferee';
|
||||
import { ShopUserReferee } from '@/api/shop/shopUserReferee/model';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
|
||||
import { FormInstance } from 'ant-design-vue/es/form';
|
||||
import { FileRecord } from '@/api/system/file/model';
|
||||
|
||||
// 是否是修改
|
||||
const isUpdate = ref(false);
|
||||
const useForm = Form.useForm;
|
||||
// 是否开启响应式布局
|
||||
const themeStore = useThemeStore();
|
||||
const { styleResponsive } = storeToRefs(themeStore);
|
||||
|
||||
const props = defineProps<{
|
||||
// 弹窗是否打开
|
||||
visible: boolean;
|
||||
// 修改回显的数据
|
||||
data?: ShopUserReferee | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
// 提交状态
|
||||
const loading = ref(false);
|
||||
// 是否显示最大化切换按钮
|
||||
const maxable = ref(true);
|
||||
// 表格选中数据
|
||||
const formRef = ref<FormInstance | null>(null);
|
||||
const images = ref<ItemType[]>([]);
|
||||
|
||||
// 用户信息
|
||||
const form = reactive<ShopUserReferee>({
|
||||
id: undefined,
|
||||
dealerId: undefined,
|
||||
userId: undefined,
|
||||
level: undefined,
|
||||
comments: undefined,
|
||||
deleted: undefined,
|
||||
tenantId: undefined,
|
||||
createTime: undefined,
|
||||
updateTime: undefined
|
||||
});
|
||||
|
||||
/* 更新visible */
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive({
|
||||
shopUserRefereeName: [
|
||||
{
|
||||
required: true,
|
||||
type: 'string',
|
||||
message: '请填写用户推荐关系表名称',
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const chooseImage = (data: FileRecord) => {
|
||||
images.value.push({
|
||||
uid: data.id,
|
||||
url: data.path,
|
||||
status: 'done'
|
||||
});
|
||||
form.image = data.path;
|
||||
};
|
||||
|
||||
const onDeleteItem = (index: number) => {
|
||||
images.value.splice(index, 1);
|
||||
form.image = '';
|
||||
};
|
||||
|
||||
const { resetFields } = useForm(form, rules);
|
||||
|
||||
/* 保存编辑 */
|
||||
const save = () => {
|
||||
if (!formRef.value) {
|
||||
return;
|
||||
}
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(() => {
|
||||
loading.value = true;
|
||||
const formData = {
|
||||
...form
|
||||
};
|
||||
const saveOrUpdate = isUpdate.value
|
||||
? updateShopUserReferee
|
||||
: addShopUserReferee;
|
||||
saveOrUpdate(formData)
|
||||
.then((msg) => {
|
||||
loading.value = false;
|
||||
message.success(msg);
|
||||
updateVisible(false);
|
||||
emit('done');
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
message.error(e.message);
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
images.value = [];
|
||||
if (props.data) {
|
||||
assignObject(form, props.data);
|
||||
if (props.data.image) {
|
||||
images.value.push({
|
||||
uid: uuid(),
|
||||
url: props.data.image,
|
||||
status: 'done'
|
||||
});
|
||||
}
|
||||
isUpdate.value = true;
|
||||
} else {
|
||||
isUpdate.value = false;
|
||||
}
|
||||
} else {
|
||||
resetFields();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
263
src/views/shop/shopUserReferee/index.vue
Normal file
263
src/views/shop/shopUserReferee/index.vue
Normal file
@@ -0,0 +1,263 @@
|
||||
<template>
|
||||
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
|
||||
<a-card :bordered="false" :body-style="{ padding: '16px' }">
|
||||
<ele-pro-table
|
||||
ref="tableRef"
|
||||
row-key="shopUserRefereeId"
|
||||
:columns="columns"
|
||||
:datasource="datasource"
|
||||
:customRow="customRow"
|
||||
tool-class="ele-toolbar-form"
|
||||
class="sys-org-table"
|
||||
>
|
||||
<template #toolbar>
|
||||
<search
|
||||
@search="reload"
|
||||
:selection="selection"
|
||||
@add="openEdit"
|
||||
@remove="removeBatch"
|
||||
@batchMove="openMove"
|
||||
/>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'image'">
|
||||
<a-image :src="record.image" :width="50" />
|
||||
</template>
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag v-if="record.status === 0" color="green">显示</a-tag>
|
||||
<a-tag v-if="record.status === 1" color="red">隐藏</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a @click="openEdit(record)">修改</a>
|
||||
<a-divider type="vertical" />
|
||||
<a-popconfirm
|
||||
title="确定要删除此记录吗?"
|
||||
@confirm="remove(record)"
|
||||
>
|
||||
<a class="ele-text-danger">删除</a>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<ShopUserRefereeEdit
|
||||
v-model:visible="showEdit"
|
||||
:data="current"
|
||||
@done="reload"
|
||||
/>
|
||||
</a-page-header>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { createVNode, ref } from 'vue';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
|
||||
import type { EleProTable } from 'ele-admin-pro';
|
||||
import { toDateString } from 'ele-admin-pro';
|
||||
import type {
|
||||
DatasourceFunction,
|
||||
ColumnItem
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import Search from './components/search.vue';
|
||||
import { getPageTitle } from '@/utils/common';
|
||||
import ShopUserRefereeEdit from './components/shopUserRefereeEdit.vue';
|
||||
import {
|
||||
pageShopUserReferee,
|
||||
removeShopUserReferee,
|
||||
removeBatchShopUserReferee
|
||||
} from '@/api/shop/shopUserReferee';
|
||||
import type {
|
||||
ShopUserReferee,
|
||||
ShopUserRefereeParam
|
||||
} from '@/api/shop/shopUserReferee/model';
|
||||
|
||||
// 表格实例
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
|
||||
// 表格选中数据
|
||||
const selection = ref<ShopUserReferee[]>([]);
|
||||
// 当前编辑数据
|
||||
const current = ref<ShopUserReferee | null>(null);
|
||||
// 是否显示编辑弹窗
|
||||
const showEdit = ref(false);
|
||||
// 是否显示批量移动弹窗
|
||||
const showMove = ref(false);
|
||||
// 加载状态
|
||||
const loading = ref(true);
|
||||
|
||||
// 表格数据源
|
||||
const datasource: DatasourceFunction = ({
|
||||
page,
|
||||
limit,
|
||||
where,
|
||||
orders,
|
||||
filters
|
||||
}) => {
|
||||
if (filters) {
|
||||
where.status = filters.status;
|
||||
}
|
||||
return pageShopUserReferee({
|
||||
...where,
|
||||
...orders,
|
||||
page,
|
||||
limit
|
||||
});
|
||||
};
|
||||
|
||||
// 表格列配置
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
title: '主键ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
align: 'center',
|
||||
width: 90
|
||||
},
|
||||
{
|
||||
title: '推荐人ID',
|
||||
dataIndex: 'dealerId',
|
||||
key: 'dealerId',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '用户id(被推荐人)',
|
||||
dataIndex: 'userId',
|
||||
key: 'userId',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '推荐关系层级(1,2,3)',
|
||||
dataIndex: 'level',
|
||||
key: 'level',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
dataIndex: 'comments',
|
||||
key: 'comments',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '是否删除, 0否, 1是',
|
||||
dataIndex: 'deleted',
|
||||
key: 'deleted',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
align: 'center',
|
||||
sorter: true,
|
||||
ellipsis: true,
|
||||
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd')
|
||||
},
|
||||
{
|
||||
title: '修改时间',
|
||||
dataIndex: 'updateTime',
|
||||
key: 'updateTime',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 180,
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
hideInSetting: true
|
||||
}
|
||||
]);
|
||||
|
||||
/* 搜索 */
|
||||
const reload = (where?: ShopUserRefereeParam) => {
|
||||
selection.value = [];
|
||||
tableRef?.value?.reload({ where: where });
|
||||
};
|
||||
|
||||
/* 打开编辑弹窗 */
|
||||
const openEdit = (row?: ShopUserReferee) => {
|
||||
current.value = row ?? null;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
/* 打开批量移动弹窗 */
|
||||
const openMove = () => {
|
||||
showMove.value = true;
|
||||
};
|
||||
|
||||
/* 删除单个 */
|
||||
const remove = (row: ShopUserReferee) => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeShopUserReferee(row.shopUserRefereeId)
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
/* 批量删除 */
|
||||
const removeBatch = () => {
|
||||
if (!selection.value.length) {
|
||||
message.error('请至少选择一条数据');
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确定要删除选中的记录吗?',
|
||||
icon: createVNode(ExclamationCircleOutlined),
|
||||
maskClosable: true,
|
||||
onOk: () => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeBatchShopUserReferee(
|
||||
selection.value.map((d) => d.shopUserRefereeId)
|
||||
)
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/* 查询 */
|
||||
const query = () => {
|
||||
loading.value = true;
|
||||
};
|
||||
|
||||
/* 自定义行属性 */
|
||||
const customRow = (record: ShopUserReferee) => {
|
||||
return {
|
||||
// 行点击事件
|
||||
onClick: () => {
|
||||
// console.log(record);
|
||||
},
|
||||
// 行双击事件
|
||||
onDblclick: () => {
|
||||
openEdit(record);
|
||||
}
|
||||
};
|
||||
};
|
||||
query();
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ShopUserReferee'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
Reference in New Issue
Block a user