Files
guilixu-admin/src/views/system/user/index.vue
赵忠林 e87e60fd6a feat(shopOrder): 添加订单商品明细展开功能并优化订单状态显示
- 在订单列表中增加商品明细的展开/收起功能,默认显示2条商品信息
- 支持点击“展开全部 N 件”切换显示全部或折叠商品明细
- 优化订单状态栏支付状态、发货状态、开票状态及操作按钮中的分隔线统一风格
- 将shop/dashboard运行天数改为基于租户创建时间,而非订阅生效时间,保证准确性
- 调整订单列表表格列宽及显示顺序,改善界面布局和信息展示
- 注释和代码格式微调,删除无用的组件显示及注释代码,提升代码整洁度
- 修复了部分变量定义和模板标签排版,完善代码可维护性
2026-07-29 00:44:54 +08:00

839 lines
23 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
<!-- 高级筛选 -->
<a-form layout="inline" class="ele-form-inline" style="margin-bottom: 16px">
<a-form-item>
<a-select
v-model:value="searchWhere.roleId"
placeholder="全部角色"
allow-clear
style="width: 160px"
>
<a-select-option
v-for="item in roles"
:key="item.value"
:value="item.value"
>{{ item.text }}</a-select-option
>
</a-select>
</a-form-item>
<a-form-item>
<a-select
v-model:value="searchWhere.platform"
placeholder="全部来源"
allow-clear
style="width: 140px"
>
<a-select-option value="MP-WEIXIN">微信小程序</a-select-option>
<a-select-option value="H5">H5</a-select-option>
<a-select-option value="WEB">Web</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<a-select
v-model:value="searchWhere.isAdmin"
placeholder="全部"
allow-clear
style="width: 120px"
>
<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-button type="primary" @click="reload()">搜索</a-button>
<a-button style="margin-left: 8px" @click="resetSearch">重置</a-button>
</a-form-item>
</a-form>
<a-card :bordered="false">
<!-- 表格 -->
<ele-pro-table
ref="tableRef"
row-key="userId"
:columns="columns"
:datasource="datasource"
v-model:selection="selection"
:scroll="{ x: 1800 }"
: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
type="primary"
class="ele-btn-icon"
@click="openEdit(selection[0])"
:disabled="selection.length === 0"
>
<template #icon>
<EditOutlined />
</template>
<span>修改</span>
</a-button>
<a-button
type="primary"
danger
@click="resetPsw(selection[0])"
:disabled="selection.length === 0"
>重置密码
</a-button>
<a-button
danger
type="primary"
class="ele-btn-icon"
@click="removeBatch"
>
<template #icon>
<delete-outlined />
</template>
<span>批量删除</span>
</a-button>
<template v-if="hasRole('superAdmin')">
<a-button type="dashed" class="ele-btn-icon" @click="openImport">
<template #icon>
<upload-outlined />
</template>
<span>导入</span>
</a-button>
<a-button type="dashed" @click="handleExport">导出xls</a-button>
</template>
<a-input-search
allow-clear
v-model:value="searchText"
placeholder="请输入关键词"
@search="handleSearch"
@pressEnter="handleSearch"
/>
</a-space>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'userInfo'">
<div class="user-box">
<a-avatar
:size="36"
:src="`${record.avatar}`"
>
<template #icon>
<UserOutlined />
</template>
</a-avatar>
<div class="user-info">
<span class="user-name">{{ record.realName || '-' }}</span>
<span class="user-mobile">{{ hasRole('superAdmin') ? record.phone : record.mobile }}</span>
</div>
</div>
</template>
<template v-if="column.key === 'nickname'">
<span>{{ record.nickname }}</span>
</template>
<template v-if="column.key === 'storeName'">
<span :title="record.storeName">{{ record.storeName || '-' }}</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 === 'status'">
<a-tag v-if="record.status === 0" color="green">正常</a-tag>
<a-tag v-else-if="record.status === 1" color="red">冻结</a-tag>
<span v-else>-</span>
</template>
<template v-if="column.key === 'auditStatus'">
<a-tag v-if="record.auditStatus === 0" color="orange">待审核</a-tag>
<a-tag v-else-if="record.auditStatus === 1" color="green">通过</a-tag>
<a-tag v-else-if="record.auditStatus === 2" color="red">拒绝</a-tag>
<span v-else>-</span>
</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-tag v-if="record.isAdmin"></a-tag>
<a-tag v-else></a-tag>
<!-- <a-switch-->
<!-- :checked="record.isAdmin == 1"-->
<!-- @change="updateIsAdmin(record)"-->
<!-- />-->
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical" />
<!-- <a @click="resetPsw(record)">重置</a>-->
<a-popconfirm
placement="topRight"
title="确定要删除此用户吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</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" />
<!-- 审核弹窗 -->
<a-modal
v-model:visible="showAudit"
:title="auditTitle"
@ok="submitAudit"
>
<a-form :model="auditForm" layout="vertical">
<div
v-if="!batchAudit && currentAuditUser"
class="audit-user-info"
style="margin-bottom: 16px; padding: 12px; background: #f5f5f5; border-radius: 4px;"
>
<p style="margin-bottom: 8px;">
<strong>店名</strong>{{ currentAuditUser.storeName || '-' }}
</p>
<p style="margin-bottom: 8px;">
<strong>姓名</strong>{{ currentAuditUser.realName || currentAuditUser.nickname || '-' }}
</p>
<p style="margin-bottom: 0;">
<strong>所在地区</strong>
{{ [currentAuditUser.province, currentAuditUser.city, currentAuditUser.region].filter(Boolean).join(' ') || '-' }}
</p>
</div>
<a-form-item label="审核结果">
<a-radio-group v-model:value="auditForm.status">
<a-radio :value="1">通过</a-radio>
<a-radio :value="2">拒绝</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="审核备注">
<a-textarea
v-model:value="auditForm.remark"
:rows="3"
placeholder="请输入审核备注(拒绝时建议填写)"
/>
</a-form-item>
</a-form>
</a-modal>
</a-page-header>
</template>
<script lang="ts" setup>
import { createVNode, ref, computed, reactive } from 'vue';
import { message, Modal } from 'ant-design-vue/es';
import {
PlusOutlined,
DeleteOutlined,
UploadOutlined,
EditOutlined,
UserOutlined,
Html5Outlined,
ChromeOutlined,
WechatOutlined,
ExclamationCircleOutlined,
CheckCircleOutlined
} 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 UserInfo from './components/user-info.vue';
import { toDateString } from 'ele-admin-pro';
import {
pageUsers,
removeUser,
removeUsers,
updateUserPassword,
updateUser,
listUsers,
auditUser
} from '@/api/system/user';
import type { User, UserParam } from '@/api/system/user/model';
import { toTreeData, uuid } from 'ele-admin-pro';
import { utils, writeFile } from 'xlsx';
import dayjs from 'dayjs';
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 { useUserStore } from '@/store/modules/user';
// 加载状态
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 showAudit = ref(false);
// 是否批量审核
const batchAudit = ref(false);
// 当前审核用户(单条审核时显示详情)
const currentAuditUser = ref<User | null>(null);
// 审核弹窗标题
const auditTitle = ref('审核用户');
// 审核表单
const auditForm = reactive({
userId: undefined as number | undefined,
status: 1,
remark: ''
});
const searchText = ref('');
// 高级筛选条件
const searchWhere = reactive({
roleId: undefined,
platform: undefined,
isAdmin: undefined
});
const resetSearch = () => {
searchWhere.roleId = undefined;
searchWhere.platform = undefined;
searchWhere.isAdmin = undefined;
reload();
};
const userStore = useUserStore();
// 当前用户信息
const loginUser = computed(() => userStore.info ?? {});
// 加载角色
const roles = ref<any[]>([]);
const filters = () => {
listRoles().then((result) => {
result.map((d) => {
roles.value.push({
text: d.roleName,
value: d.roleId
});
});
});
};
filters();
// 加载机构
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,
fixed: 'left',
showSorterTooltip: false
},
{
title: '用户信息',
key: 'userInfo',
fixed: 'left',
width: 220
},
{
title: '昵称',
key: 'nickname',
dataIndex: 'nickname'
},
{
title: '性别',
dataIndex: 'sex',
showSorterTooltip: false,
customRender: ({ text }) => ['', '男', '女'][text]
},
{
title: '邮箱',
dataIndex: 'email',
showSorterTooltip: false
},
{
title: '可用余额',
dataIndex: 'balance',
sorter: true
},
{
title: '可用积分',
dataIndex: 'points',
sorter: true
},
{
title: '实际消费金额',
dataIndex: 'expendMoney',
key: 'expendMoney',
sorter: true,
width: 120,
showSorterTooltip: false
},
{
title: '注册来源',
key: 'platform',
dataIndex: 'platform',
width: 120
},
{
title: '证件号码',
dataIndex: 'idCard',
hideInTable: true
},
{
title: '出生日期',
dataIndex: 'birthday',
key: 'birthday',
hideInTable: true
},
{
title: '省份',
dataIndex: 'province',
key: 'province',
},
{
title: '城市',
dataIndex: 'city',
key: 'city',
showSorterTooltip: false
},
{
title: '地区',
dataIndex: 'region',
key: 'region',
showSorterTooltip: false
},
{
title: '邮箱认证',
dataIndex: 'emailVerified',
hideInTable: true,
showSorterTooltip: false,
customRender: ({ text }) => ['未认证', '已认证'][text]
},
{
title: '实名认证',
dataIndex: 'certification',
sorter: true,
hideInTable: true,
customRender: ({ text }) => ['未认证', '已认证'][text]
},
{
title: '角色',
dataIndex: 'roles',
key: 'roles',
align: 'center'
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
align: 'center',
width: 90
},
{
title: '管理员',
key: 'isAdmin',
dataIndex: 'isAdmin',
sorter: true,
hideInTable: true,
showSorterTooltip: false,
align: 'center'
},
{
title: '注册时间',
dataIndex: 'createTime',
sorter: true,
showSorterTooltip: false,
ellipsis: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
{
title: '操作',
key: 'action',
width: 160,
fixed: 'right',
align: 'center'
}
]);
// 默认搜索条件
const defaultWhere = reactive({
username: '',
nickname: ''
});
// 表格数据源
const datasource: DatasourceFunction = ({ page, limit, where, orders }) => {
const params = {
...where,
keywords: searchText.value,
roleId: searchWhere.roleId,
platform: searchWhere.platform,
isAdmin: searchWhere.isAdmin || undefined,
auditStatus: 1
};
return pageUsers({ page, limit, ...params, ...orders });
};
/* 搜索 */
const reload = (where?: UserParam) => {
selection.value = [];
tableRef?.value?.reload({ where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: User) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开用户详情弹窗 */
const openInfo = (row?: User) => {
current.value = row ?? null;
showInfo.value = true;
};
/* 打开编辑弹窗 */
const openImport = () => {
showImport.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 removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的用户吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = messageLoading('请求中..', 0);
removeUsers(selection.value.map((d) => d.userId))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 重置用户密码 */
const resetPsw = (row: User) => {
Modal.confirm({
title: '提示',
content: '确定要重置此用户的密码吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
const password = uuid(8);
updateUserPassword(row.userId, password)
.then(() => {
hide();
Modal.success({
title: '密码重置成功',
content: `新密码:${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 openAudit = (row: User) => {
batchAudit.value = false;
currentAuditUser.value = row;
auditForm.userId = row.userId;
auditForm.status = 1;
auditForm.remark = '';
auditTitle.value = `审核用户 - ${row.nickname || row.username}`;
showAudit.value = true;
};
/* 打开批量审核弹窗 */
const openBatchAudit = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
batchAudit.value = true;
currentAuditUser.value = null;
auditForm.userId = undefined;
auditForm.status = 1;
auditForm.remark = '';
auditTitle.value = `批量审核(已选 ${selection.value.length} 条)`;
showAudit.value = true;
};
/* 提交审核 */
const submitAudit = () => {
const hide = message.loading('提交中..', 0);
const doOne = (userId: number) =>
auditUser(userId, { status: auditForm.status, remark: auditForm.remark });
const tasks = batchAudit.value
? selection.value.map((d) => doOne(d.userId as number))
: [doOne(auditForm.userId as number)];
Promise.all(tasks)
.then(() => {
hide();
message.success('审核完成');
showAudit.value = false;
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 搜索处理 */
const handleSearch = () => {
reload();
};
/* 导出功能 */
const handleExport = async () => {
if (loading.value) {
return;
}
loading.value = true;
message.loading('正在准备导出数据...', 0);
try {
const array: (string | number)[][] = [
[
'用户ID',
'头像',
'手机号码',
'真实姓名',
'昵称',
'性别',
'邮箱',
'可用余额',
'可用积分',
'注册来源',
'注册时间'
]
];
// 构建查询参数
const queryParams = {
keywords: searchText.value
};
// 按搜索结果导出
const list = await listUsers(queryParams);
if (!list || list.length === 0) {
message.warning('没有数据可以导出');
loading.value = false;
return;
}
list.forEach((user: User) => {
array.push([
`${user.userId || ''}`,
`${user.avatar ? '有头像' : '无头像'}`,
`${user.phone || ''}`,
`${user.realName || ''}`,
`${user.nickname || ''}`,
`${user.sex === '1' ? '男' : user.sex === '2' ? '女' : ''}`,
`${user.email || ''}`,
`${user.balance || '0'}`,
`${user.points || '0'}`,
`${user.platform || ''}`,
`${user.createTime || ''}`
]);
});
const sheetName = `导出用户列表${dayjs(new Date()).format('YYYYMMDD')}`;
const workbook = {
SheetNames: [sheetName],
Sheets: {}
};
const sheet = utils.aoa_to_sheet(array);
workbook.Sheets[sheetName] = sheet;
// 设置列宽
sheet['!cols'] = [
{ wch: 10 }, // 用户ID
{ wch: 10 }, // 头像
{ wch: 15 }, // 手机号码
{ wch: 15 }, // 真实姓名
{ wch: 15 }, // 昵称
{ wch: 8 }, // 性别
{ wch: 20 }, // 邮箱
{ wch: 12 }, // 可用余额
{ wch: 12 }, // 可用积分
{ wch: 12 }, // 注册来源
{ wch: 20 } // 注册时间
];
message.destroy();
message.loading('正在生成Excel文件...', 0);
setTimeout(() => {
writeFile(workbook, `${sheetName}.xlsx`);
loading.value = false;
message.destroy();
message.success(`成功导出 ${list.length} 条记录`);
}, 1000);
} catch (error: any) {
loading.value = false;
message.destroy();
message.error(error.message || '导出失败,请重试');
}
};
/* 自定义行属性 */
const customRow = (record: User) => {
return {
// 行点击查看用户详情
// onClick: () => {
// openInfo(record);
// },
// 行双击事件
onDblclick: () => {
openInfo(record);
}
};
};
</script>
<script lang="ts">
export default {
name: 'SystemUser'
};
</script>
<style lang="less" scoped>
.user-box {
display: flex;
align-items: center;
gap: 10px;
.user-info {
display: flex;
flex-direction: column;
align-items: flex-start;
line-height: 1.5;
.user-name {
font-weight: 500;
color: var(--text-color);
}
.user-mobile {
font-size: 12px;
color: #999;
}
}
}
</style>