1
This commit is contained in:
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>
|
||||
Reference in New Issue
Block a user