feat: add web application

This commit is contained in:
richjohnrambo
2026-10-10 20:55:59 +08:00
parent 3eb5d78875
commit 50fcfd2d3d
571 changed files with 73992 additions and 135 deletions
+232
View File
@@ -0,0 +1,232 @@
<template>
<div>
<n-card :bordered="false" class="proCard" title="部门管理">
<BasicForm
ref="searchFormRef"
@register="register"
@submit="reloadTable"
@reset="reloadTable"
@keyup.enter="reloadTable"
>
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
<BasicTable
ref="actionRef"
openChecked
:columns="columns"
:pagination="false"
:request="loadDataTable"
:row-key="(row) => row.id"
:actionColumn="actionColumn"
:scroll-x="1280"
:resizeHeightOffset="-10000"
:cascade="false"
:expanded-row-keys="expandedKeys"
@update:expanded-row-keys="updateExpandedKeys"
:checked-row-keys="checkedIds"
@update:checked-row-keys="handleOnCheckedRow"
>
<template #tableTitle>
<n-button
type="primary"
@click="addTable"
class="min-left-space"
v-if="hasPermission(['/dept/edit'])"
>
<template #icon>
<n-icon>
<PlusOutlined />
</n-icon>
</template>
添加部门
</n-button>
<n-button
type="error"
@click="handleBatchDelete"
class="min-left-space"
v-if="hasPermission(['/dept/delete'])"
>
<template #icon>
<n-icon>
<DeleteOutlined />
</n-icon>
</template>
批量删除
</n-button>
<n-button
type="primary"
icon-placement="left"
@click="handleAllExpanded"
class="min-left-space"
>
全部{{ expandedKeys.length ? '收起' : '展开' }}
<template #icon>
<div class="flex items-center">
<n-icon size="14">
<AlignLeftOutlined />
</n-icon>
</div>
</template>
</n-button>
</template>
</BasicTable>
</n-card>
<Edit ref="editRef" @reloadTable="reloadTable" />
</div>
</template>
<script lang="ts" setup>
import { h, reactive, ref, onMounted } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, useForm } from '@/components/Form/index';
import { usePermission } from '@/hooks/web/usePermission';
import { getDeptList, Delete } from '@/api/org/dept';
import { PlusOutlined, DeleteOutlined, AlignLeftOutlined } from '@vicons/antd';
import { columns, schemas, loadOptions, newState, filterIds } from './model';
import { convertListToTree } from '@/utils/hotgo';
import Edit from './edit.vue';
const dialog = useDialog();
const message = useMessage();
const { hasPermission } = usePermission();
const actionRef = ref();
const searchFormRef = ref<any>({});
const editRef = ref();
const checkedIds = ref([]);
const expandedKeys = ref([]);
const allTreeKeys = ref([]);
const actionColumn = reactive({
width: 220,
title: '操作',
key: 'action',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
auth: ['/dept/edit'],
},
{
label: '添加子部门',
onClick: handleAdd.bind(null, record),
auth: ['/dept/edit'],
},
{
label: '删除',
onClick: handleDelete.bind(null, record),
auth: ['/dept/delete'],
},
],
});
},
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
// 加载普通数表数据
const loadDataTable = async (res = {}) => {
filterIds.value = [];
const params = { ...(searchFormRef.value?.formModel ?? {}), ...res, pagination: false };
const dataSource = await getDeptList(params);
allTreeKeys.value = expandedKeys.value = dataSource.list.map((item) => item.id);
dataSource.list = convertListToTree(dataSource.list, 'id');
filterIds.value = dataSource.ids;
return dataSource;
};
// 更新选中的行
function handleOnCheckedRow(rowKeys) {
checkedIds.value = rowKeys;
}
// 重新加载表格数据
function reloadTable() {
actionRef.value?.reload();
}
// 添加数据
function addTable() {
editRef.value.openModal(null);
}
// 添加树节点下级数据
function handleAdd(record: Recordable) {
const state = newState(null);
state.pid = record.id;
editRef.value.openModal(state);
}
// 编辑数据
function handleEdit(record: Recordable) {
editRef.value.openModal(record);
}
// 单个删除
function handleDelete(record: Recordable) {
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
Delete(record).then((_res) => {
message.success('删除成功');
reloadTable();
});
},
});
}
// 批量删除
function handleBatchDelete() {
if (checkedIds.value.length < 1) {
message.error('请至少选择一项要删除的数据');
return;
}
dialog.warning({
title: '警告',
content: '你确定要批量删除?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
Delete({ id: checkedIds.value }).then((_res) => {
checkedIds.value = [];
message.success('删除成功');
reloadTable();
});
},
});
}
// 收起/展开全部树节点
function handleAllExpanded() {
if (expandedKeys.value.length) {
expandedKeys.value = [];
} else {
expandedKeys.value = allTreeKeys.value;
}
}
// 更新展开的树节点
function updateExpandedKeys(openKeys: never[]) {
expandedKeys.value = openKeys;
}
onMounted(() => {
loadOptions();
});
</script>
<style lang="less" scoped></style>
+194
View File
@@ -0,0 +1,194 @@
<template>
<div>
<n-modal
v-model:show="showModal"
:mask-closable="false"
:show-icon="false"
preset="dialog"
transform-origin="center"
:title="formValue.id > 0 ? '编辑部门 #' + formValue.id : '添加部门'"
:style="{
width: dialogWidth,
}"
>
<n-scrollbar style="max-height: 87vh" class="pr-5">
<n-spin :show="loading" description="请稍候...">
<n-form
ref="formRef"
:model="formValue"
:rules="rules"
:label-placement="settingStore.isMobile ? 'top' : 'left'"
:label-width="100"
class="py-4"
>
<n-grid cols="1 s:1 m:1 l:1 xl:1 2xl:1" responsive="screen">
<n-gi span="1">
<n-form-item label="上级部门" path="pid">
<n-tree-select
:options="treeOption"
v-model:value="formValue.pid"
key-field="id"
label-field="name"
clearable
filterable
default-expand-all
show-path
/>
</n-form-item>
</n-gi>
<n-gi span="1">
<n-form-item label="部门名称" path="name">
<n-input placeholder="请输入部门名称" v-model:value="formValue.name" />
</n-form-item>
</n-gi>
<n-gi span="1">
<n-form-item label="部门编码" path="code">
<n-input placeholder="请输入部门编码" v-model:value="formValue.code" />
</n-form-item>
</n-gi>
<n-gi span="1">
<n-form-item label="部门类型" path="type">
<n-radio-group v-model:value="formValue.type" name="type">
<n-space>
<n-radio v-for="item in dict.getOptionUnRef('deptType')" :value="item.value">
{{ item.label }}
</n-radio>
</n-space>
</n-radio-group>
</n-form-item>
</n-gi>
<n-gi span="1">
<n-form-item label="负责人" path="leader">
<n-input placeholder="请输入负责人" v-model:value="formValue.leader" />
</n-form-item>
</n-gi>
<n-gi span="1">
<n-form-item label="联系电话" path="phone">
<n-input placeholder="请输入联系电话" v-model:value="formValue.phone" />
</n-form-item>
</n-gi>
<n-gi span="1">
<n-form-item label="邮箱" path="email">
<n-input placeholder="请输入邮箱" v-model:value="formValue.email" />
</n-form-item>
</n-gi>
<n-gi span="1">
<n-form-item label="排序" path="sort">
<n-input-number
placeholder="请输入排序"
v-model:value="formValue.sort"
clearable
style="width: 100%"
/>
</n-form-item>
</n-gi>
<n-gi span="1">
<n-form-item label="状态" path="status">
<n-radio-group v-model:value="formValue.status" name="status">
<n-radio-button
v-for="status in dict.getOptionUnRef('sys_normal_disable')"
:key="status.value"
:value="status.value"
:label="status.label"
/>
</n-radio-group>
</n-form-item>
</n-gi>
</n-grid>
</n-form>
</n-spin>
</n-scrollbar>
<template #action>
<n-space>
<n-button @click="closeForm"> 取消 </n-button>
<n-button type="info" :loading="formBtnLoading" @click="confirmForm"> 确定 </n-button>
</n-space>
</template>
</n-modal>
</div>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import { Edit, View, MaxSort } from '@/api/org/dept';
import { State, newState, treeOption, loadTreeOption, rules } from './model';
import { useProjectSettingStore } from '@/store/modules/projectSetting';
import { useMessage } from 'naive-ui';
import { adaModalWidth } from '@/utils/hotgo';
import { useDictStore } from '@/store/modules/dict';
const emit = defineEmits(['reloadTable']);
const message = useMessage();
const settingStore = useProjectSettingStore();
const dict = useDictStore();
const loading = ref(false);
const showModal = ref(false);
const formValue = ref<State>(newState(null));
const formRef = ref<any>({});
const formBtnLoading = ref(false);
const dialogWidth = computed(() => {
return adaModalWidth(520);
});
function openModal(state: State) {
showModal.value = true;
// 加载关系树选项
loadTreeOption();
// 新增
if (!state || state.id < 1) {
formValue.value = newState(state);
loading.value = true;
MaxSort()
.then((res) => {
formValue.value.sort = res.sort;
})
.finally(() => {
loading.value = false;
});
return;
}
// 编辑
loading.value = true;
View({ id: state.id })
.then((res) => {
formValue.value = res;
})
.finally(() => {
loading.value = false;
});
}
function confirmForm(e) {
e.preventDefault();
formBtnLoading.value = true;
formRef.value.validate((errors) => {
if (!errors) {
Edit(formValue.value).then((_res) => {
message.success('操作成功');
setTimeout(() => {
closeForm();
emit('reloadTable');
});
});
} else {
message.error('请填写完整信息');
}
formBtnLoading.value = false;
});
}
function closeForm() {
showModal.value = false;
loading.value = false;
}
defineExpose({
openModal,
});
</script>
<style lang="less"></style>
+201
View File
@@ -0,0 +1,201 @@
import { h, ref } from 'vue';
import { NTag, NButton } from 'naive-ui';
import { cloneDeep } from 'lodash-es';
import { FormSchema } from '@/components/Form';
import { defRangeShortcuts } from '@/utils/dateUtil';
import { validate } from '@/utils/validateUtil';
import { renderTooltip, renderIcon, renderOptionTag } from '@/utils';
import { HelpCircleOutline } from '@vicons/ionicons5';
import { TreeOption } from '@/api/org/dept';
import { useDictStore } from '@/store/modules/dict';
import type { FormRules } from 'naive-ui/es/form/src/interface';
const dict = useDictStore();
export class State {
public id = 0; // 部门ID
public pid = 0; // 父部门ID
public name = ''; // 部门名称
public code = ''; // 部门编码
public type = 'company'; // 部门类型
public leader = ''; // 负责人
public phone = ''; // 联系电话
public email = ''; // 邮箱
public level = 0; // 关系树等级
public tree = ''; // 关系树
public sort = 0; // 排序
public status = 1; // 部门状态
public createdAt = ''; // 创建时间
public updatedAt = ''; // 更新时间
constructor(state?: Partial<State>) {
if (state) {
Object.assign(this, state);
}
}
}
export function newState(state: State | Record<string, any> | null): State {
if (state !== null) {
if (state instanceof State) {
return cloneDeep(state);
}
return new State(state);
}
return new State();
}
// 表单验证规则
export const rules: FormRules = {
email: {
required: false,
trigger: ['blur', 'input'],
type: 'string',
validator: validate.email,
},
};
// 表格搜索表单
export const schemas = ref<FormSchema[]>([
{
field: 'name',
component: 'NInput',
label: '部门名称',
componentProps: {
placeholder: '请输入部门名称',
onInput: (e: any) => {
console.log(e);
},
},
rules: [{ message: '请输入部门名称', trigger: ['blur'] }],
},
{
field: 'code',
component: 'NInput',
label: '部门编码',
componentProps: {
placeholder: '请输入部门编码',
showButton: false,
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'leader',
component: 'NInput',
label: '负责人',
componentProps: {
placeholder: '请输入负责人',
showButton: false,
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'createdAt',
component: 'NDatePicker',
label: '创建时间',
componentProps: {
type: 'datetimerange',
clearable: true,
shortcuts: defRangeShortcuts(),
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
]);
export const filterIds = ref([]);
// 表格列
export const columns = [
{
title(_column) {
return renderTooltip(
h(
NButton,
{
strong: true,
size: 'small',
text: true,
iconPlacement: 'right',
},
{ default: () => '部门名称', icon: renderIcon(HelpCircleOutline) }
),
'支持上下级部门,点击列表中左侧 > 按钮可展开下级部门列表'
);
},
key: 'name',
render(row) {
const filter = filterIds.value.includes(row.id as never);
return h(
NTag,
{
type: 'info',
checkable: filter,
checked: filter,
},
{
default: () => row.name,
}
);
},
width: 200,
},
{
title: '部门编码',
key: 'code',
width: 100,
},
{
title: '部门类型',
key: 'type',
align: 'left',
width: 100,
render(row) {
return renderOptionTag('deptType', row.type);
},
},
{
title: '负责人',
key: 'leader',
width: 100,
},
{
title: '联系电话',
key: 'phone',
width: 150,
},
{
title: '状态',
key: 'status',
align: 'left',
width: 80,
render(row) {
return renderOptionTag('sys_normal_disable', row.status);
},
},
{
title: '创建时间',
key: 'createdAt',
width: 150,
},
];
// 加载字典数据选项
export function loadOptions() {
dict.loadOptions(['sys_normal_disable', 'deptType']);
}
// 关系树选项
export const treeOption = ref([]);
// 加载关系树选项
export function loadTreeOption() {
TreeOption().then((res) => {
treeOption.value = res;
});
}
+61
View File
@@ -0,0 +1,61 @@
import { h } from 'vue';
import { NTag } from 'naive-ui';
export const columns = [
{
title: 'ID',
key: 'id',
width: 100,
},
{
title: '岗位',
key: 'name',
width: 200,
render(row) {
return h(
NTag,
{
type: 'info',
},
{
default: () => row.name,
}
);
},
},
{
title: '岗位编码',
key: 'code',
width: 100,
},
{
title: '状态',
key: 'status',
width: 100,
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: row.status == 1 ? 'info' : 'error',
bordered: false,
},
{
default: () => (row.status == 1 ? '正常' : '已禁用'),
}
);
},
},
{
title: '备注',
key: 'sort',
width: 150,
},
{
title: '创建时间',
key: 'createdAt',
width: 180,
},
];
+305
View File
@@ -0,0 +1,305 @@
<template>
<div>
<n-card :bordered="false" class="proCard" title="岗位管理">
<BasicForm
@register="register"
@submit="handleSubmit"
@reset="handleReset"
@keyup.enter="handleSubmit"
ref="searchFormRef"
>
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
<BasicTable
:openChecked="true"
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:resizeHeightOffset="-10000"
:scroll-x="1090"
>
<template #tableTitle>
<n-button type="primary" @click="addTable">
<template #icon>
<n-icon>
<PlusOutlined />
</n-icon>
</template>
添加岗位
</n-button>
&nbsp;
<n-button type="error" @click="batchDelete" :disabled="batchDeleteDisabled">
<template #icon>
<n-icon>
<DeleteOutlined />
</n-icon>
</template>
批量删除
</n-button>
</template>
</BasicTable>
<n-modal
v-model:show="showModal"
:show-icon="false"
preset="dialog"
:title="formParams?.id > 0 ? '编辑岗位 #' + formParams?.id : '添加岗位'"
>
<n-form
:model="formParams"
ref="formRef"
label-placement="left"
:label-width="80"
class="py-4"
>
<n-form-item label="岗位名称" path="name">
<n-input placeholder="请输入岗位名称" v-model:value="formParams.name" />
</n-form-item>
<n-form-item label="岗位编码" path="code">
<n-input placeholder="请输入岗位编码" v-model:value="formParams.code" />
</n-form-item>
<n-form-item label="排序" path="sort">
<n-input-number v-model:value="formParams.sort" clearable style="width: 100%" />
</n-form-item>
<n-form-item label="状态" path="status">
<n-radio-group v-model:value="formParams.status" name="status">
<n-radio-button
v-for="status in statusOptions"
:key="status.value"
:value="status.value"
:label="status.label"
/>
</n-radio-group>
</n-form-item>
<n-form-item label="备注" path="remark">
<n-input type="textarea" placeholder="请输入备注" v-model:value="formParams.remark" />
</n-form-item>
</n-form>
<template #action>
<n-space>
<n-button @click="() => (showModal = false)">取消</n-button>
<n-button type="info" :loading="formBtnLoading" @click="confirmForm">确定</n-button>
</n-space>
</template>
</n-modal>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { h, reactive, ref } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
import { Delete, Edit, getPostList } from '@/api/org/post';
import { columns } from './columns';
import { DeleteOutlined, PlusOutlined } from '@vicons/antd';
import { statusOptions } from '@/enums/optionsiEnum';
import { defRangeShortcuts } from '@/utils/dateUtil';
import { cloneDeep } from 'lodash-es';
const params = ref<any>({
pageSize: 10,
name: '',
code: '',
status: null,
});
const schemas: FormSchema[] = [
{
field: 'name',
component: 'NInput',
label: '岗位名称',
componentProps: {
placeholder: '请输入岗位名称',
onUpdateValue: (e: any) => {
console.log(e);
},
},
rules: [{ message: '请输入岗位名称', trigger: ['blur'] }],
},
{
field: 'code',
component: 'NInput',
label: '岗位编码',
componentProps: {
placeholder: '请输入岗位编码',
showButton: false,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'status',
component: 'NSelect',
label: '状态',
defaultValue: null,
componentProps: {
placeholder: '请选择类型',
options: statusOptions,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'createdAt',
component: 'NDatePicker',
label: '创建时间',
componentProps: {
type: 'datetimerange',
clearable: true,
shortcuts: defRangeShortcuts(),
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
];
const message = useMessage();
const actionRef = ref();
const dialog = useDialog();
const showModal = ref(false);
const formBtnLoading = ref(false);
const searchFormRef = ref<any>({});
const formRef = ref<any>({});
const batchDeleteDisabled = ref(true);
const checkedIds = ref([]);
const resetFormParams = {
id: 0,
code: '',
name: '',
remark: '',
sort: 10,
status: 1,
};
const formParams = ref<any>(resetFormParams);
const actionColumn = reactive({
width: 150,
title: '操作',
key: 'action',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
},
{
label: '删除',
onClick: handleDelete.bind(null, record),
},
],
});
},
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
function addTable() {
showModal.value = true;
formParams.value = cloneDeep(resetFormParams);
}
const loadDataTable = async (res) => {
return await getPostList({ ...params.value, ...res, ...searchFormRef.value?.formModel });
};
function onCheckedRow(rowKeys) {
console.log(rowKeys);
batchDeleteDisabled.value = rowKeys.length <= 0;
checkedIds.value = rowKeys;
}
function reloadTable() {
actionRef.value.reload();
}
function confirmForm(e) {
e.preventDefault();
formBtnLoading.value = true;
formRef.value.validate((errors) => {
if (!errors) {
Edit(formParams.value).then((_res) => {
message.success('操作成功');
setTimeout(() => {
showModal.value = false;
reloadTable();
formParams.value = ref(resetFormParams);
});
});
} else {
message.error('请填写完整信息');
}
formBtnLoading.value = false;
});
}
function handleEdit(record: Recordable) {
showModal.value = true;
formParams.value = record;
}
function handleDelete(record: Recordable) {
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
Delete(record).then((_res) => {
message.success('操作成功');
reloadTable();
});
},
});
}
function batchDelete() {
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
Delete({ id: checkedIds.value }).then((_res) => {
message.success('操作成功');
reloadTable();
});
},
});
}
function handleSubmit(values: Recordable) {
console.log(values);
params.value = values;
reloadTable();
}
function handleReset(values: Recordable) {
params.value = values;
reloadTable();
}
</script>
<style lang="less" scoped></style>
+147
View File
@@ -0,0 +1,147 @@
<template>
<div>
<n-spin :show="loading" description="请稍候...">
<n-modal
v-model:show="isShowModal"
:show-icon="false"
preset="dialog"
title="变更余额"
:style="{
width: dialogWidth,
}"
>
<n-alert :show-icon="false" type="info">
通过扣除或增加你的余额来为
<b> {{ params.realName }}</b> 加款或扣款。当扣款方余额不足时,则会操作失败
</n-alert>
<n-form
:model="params"
:rules="rules"
ref="formRef"
label-placement="left"
:label-width="80"
class="py-4"
>
<n-form-item label="管理员ID" path="id">
<n-input v-model:value="params.id" :disabled="true" />
</n-form-item>
<n-form-item label="TA的余额" path="balance">
<n-input placeholder="请输入" v-model:value="params.balance" :disabled="true" />
</n-form-item>
<n-form-item label="操作方式" path="operateMode">
<n-radio-group v-model:value="params.operateMode" name="operateMode">
<n-radio-button
v-for="status in operateModes"
:key="status.value"
:value="status.value"
:label="status.label"
/>
</n-radio-group>
</n-form-item>
<n-form-item label="操作数量" path="num">
<n-input placeholder="请输入操作数量" v-model:value="params.num" />
</n-form-item>
</n-form>
<template #action>
<n-space>
<n-button @click="closeForm">取消</n-button>
<n-button type="info" :loading="formBtnLoading" @click="confirmForm">确定</n-button>
</n-space>
</template>
</n-modal>
</n-spin>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, watch } from 'vue';
import {
addRules as rules,
addState as State,
addNewState as newState,
operateModes,
} from './model';
import { useMessage } from 'naive-ui';
import { adaModalWidth } from '@/utils/hotgo';
import { GetMemberView, AddMemberBalance } from '@/api/org/user';
const emit = defineEmits(['reloadTable', 'updateShowModal']);
interface Props {
showModal: boolean;
formParams?: State;
}
const props = withDefaults(defineProps<Props>(), {
showModal: false,
formParams: () => {
return newState(null);
},
});
const isShowModal = computed({
get: () => {
return props.showModal;
},
set: (value) => {
emit('updateShowModal', value);
},
});
const loading = ref(false);
const params = ref<State>(props.formParams);
const message = useMessage();
const formRef = ref<any>({});
const formBtnLoading = ref(false);
const dialogWidth = computed(() => {
return adaModalWidth();
});
function confirmForm(e) {
e.preventDefault();
formBtnLoading.value = true;
formRef.value.validate((errors) => {
if (!errors) {
AddMemberBalance(params.value).then((_res) => {
message.success('操作成功');
setTimeout(() => {
isShowModal.value = false;
emit('reloadTable');
});
});
} else {
message.error('请填写完整信息');
}
formBtnLoading.value = false;
});
}
function closeForm() {
isShowModal.value = false;
}
function loadForm(value) {
loading.value = true;
GetMemberView({ id: value.id })
.then((res) => {
params.value = res;
params.value.operateMode = 1;
})
.finally(() => {
loading.value = false;
});
}
watch(
() => props.formParams,
(value) => {
if (isShowModal.value) {
loadForm(value);
}
}
);
</script>
<style lang="less"></style>
+147
View File
@@ -0,0 +1,147 @@
<template>
<div>
<n-spin :show="loading" description="请稍候...">
<n-modal
v-model:show="isShowModal"
:show-icon="false"
preset="dialog"
title="变更积分"
:style="{
width: dialogWidth,
}"
>
<n-alert :show-icon="false" type="info">
通过扣除或增加你的积分来为
<b> {{ params.realName }}</b> 加款或扣款。当扣款方积分不足时,则会操作失败
</n-alert>
<n-form
:model="params"
:rules="rules"
ref="formRef"
label-placement="left"
:label-width="80"
class="py-4"
>
<n-form-item label="管理员ID" path="id">
<n-input v-model:value="params.id" :disabled="true" />
</n-form-item>
<n-form-item label="TA的积分" path="integral">
<n-input placeholder="请输入" v-model:value="params.integral" :disabled="true" />
</n-form-item>
<n-form-item label="操作方式" path="operateMode">
<n-radio-group v-model:value="params.operateMode" name="operateMode">
<n-radio-button
v-for="status in operateModes"
:key="status.value"
:value="status.value"
:label="status.label"
/>
</n-radio-group>
</n-form-item>
<n-form-item label="操作数量" path="num">
<n-input placeholder="请输入操作数量" v-model:value="params.num" />
</n-form-item>
</n-form>
<template #action>
<n-space>
<n-button @click="closeForm">取消</n-button>
<n-button type="info" :loading="formBtnLoading" @click="confirmForm">确定</n-button>
</n-space>
</template>
</n-modal>
</n-spin>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, computed, watch } from 'vue';
import {
addRules as rules,
addState as State,
addNewState as newState,
operateModes,
} from './model';
import { useMessage } from 'naive-ui';
import { adaModalWidth } from '@/utils/hotgo';
import { GetMemberView, AddMemberIntegral } from '@/api/org/user';
const emit = defineEmits(['reloadTable', 'updateShowModal']);
interface Props {
showModal: boolean;
formParams?: State;
}
const props = withDefaults(defineProps<Props>(), {
showModal: false,
formParams: () => {
return newState(null);
},
});
const isShowModal = computed({
get: () => {
return props.showModal;
},
set: (value) => {
emit('updateShowModal', value);
},
});
const loading = ref(false);
const params = ref<State>(props.formParams);
const message = useMessage();
const formRef = ref<any>({});
const formBtnLoading = ref(false);
const dialogWidth = computed(() => {
return adaModalWidth();
});
function confirmForm(e) {
e.preventDefault();
formBtnLoading.value = true;
formRef.value.validate((errors) => {
if (!errors) {
AddMemberIntegral(params.value).then((_res) => {
message.success('操作成功');
setTimeout(() => {
isShowModal.value = false;
emit('reloadTable');
});
});
} else {
message.error('请填写完整信息');
}
formBtnLoading.value = false;
});
}
function closeForm() {
isShowModal.value = false;
}
function loadForm(value) {
loading.value = true;
GetMemberView({ id: value.id })
.then((res) => {
params.value = res;
params.value.operateMode = 1;
})
.finally(() => {
loading.value = false;
});
}
watch(
() => props.formParams,
(value) => {
if (isShowModal.value) {
loadForm(value);
}
}
);
</script>
<style lang="less"></style>
+148
View File
@@ -0,0 +1,148 @@
import { h } from 'vue';
import { NAvatar, NTag, NText } from 'naive-ui';
import { formatBefore } from '@/utils/dateUtil';
export const columns = [
{
title: '管理员ID',
key: 'id',
width: 100,
},
{
title: '用户名',
key: 'username',
width: 100,
},
{
title: '姓名',
key: 'realName',
width: 100,
render(row) {
if (row.realName == '') {
return h(NText, { depth: 3 }, { default: () => '未设置' });
}
return row.realName;
},
},
{
title: '头像',
key: 'avatar',
width: 70,
render(row) {
if (row.avatar !== '') {
return h(NAvatar, {
circle: true,
size: 'small',
src: row.avatar,
});
} else {
return h(
NAvatar,
{
circle: true,
size: 'small',
},
{
default: () =>
row.realName !== '' ? row.realName.substring(0, 1) : row.username.substring(0, 2),
}
);
}
},
},
{
title: '绑定角色',
key: 'roleName',
width: 100,
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: 'success',
bordered: false,
},
{
default: () => row.roleName,
}
);
},
},
{
title: '所属部门',
key: 'deptName',
width: 100,
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: 'info',
bordered: false,
},
{
default: () => row.deptName,
}
);
},
},
{
title: '余额',
key: 'balance',
width: 120,
render(row) {
return '¥' + Number(row.balance).toFixed(2);
},
},
{
title: '积分',
key: 'integral',
width: 120,
render(row) {
return Number(row.integral).toFixed(2);
},
},
{
title: '状态',
key: 'status',
width: 80,
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: row.status == 1 ? 'info' : 'error',
bordered: false,
},
{
default: () => (row.status == 1 ? '正常' : '已禁用'),
}
);
},
},
{
title: '最近活跃',
key: 'lastActiveAt',
width: 100,
render(row) {
if (row.lastActiveAt === null) {
return '从未登录';
}
return formatBefore(new Date(row.lastActiveAt));
},
},
{
title: '创建时间',
key: 'createdAt',
width: 180,
render: (rows, _) => {
return rows.createdAt;
},
},
];
+526
View File
@@ -0,0 +1,526 @@
<template>
<div>
<BasicForm
@register="register"
@submit="handleSubmit"
@reset="handleReset"
@keyup.enter="handleSubmit"
ref="searchFormRef"
>
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
<BasicTable
:openChecked="true"
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:scroll-x="scrollX"
:resizeHeightOffset="-10000"
>
<template #tableTitle>
<n-button
type="primary"
@click="addTable"
class="min-left-space"
v-if="hasPermission(['/member/edit'])"
>
<template #icon>
<n-icon>
<PlusOutlined />
</n-icon>
</template>
添加用户
</n-button>
<n-button
type="error"
@click="batchDelete"
:disabled="batchDeleteDisabled"
class="min-left-space"
v-if="hasPermission(['/member/delete'])"
>
<template #icon>
<n-icon>
<DeleteOutlined />
</n-icon>
</template>
批量删除
</n-button>
<n-button
type="success"
@click="handleInviteQR(userStore.info?.inviteCode)"
class="min-left-space"
v-if="userStore.loginConfig?.loginRegisterSwitch === 1"
>
<template #icon>
<n-icon>
<QrCodeOutline />
</n-icon>
</template>
邀请注册
</n-button>
</template>
</BasicTable>
<n-modal
v-model:show="showModal"
:mask-closable="false"
:show-icon="false"
preset="dialog"
:title="formParams?.id > 0 ? '编辑用户 #' + formParams?.id : '添加用户'"
:style="{
width: dialogWidth,
}"
>
<n-form
:model="formParams"
:rules="rules"
ref="formRef"
label-placement="left"
:label-width="80"
class="py-4"
>
<n-grid x-gap="24" :cols="2">
<n-gi>
<n-form-item label="用户名" path="username">
<n-input placeholder="请输入登录用户名" v-model:value="formParams.username" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="密码" path="password">
<n-input
type="password"
:placeholder="formParams.id === 0 ? '请输入' : '不填则不修改'"
v-model:value="formParams.password"
/>
</n-form-item>
</n-gi>
</n-grid>
<n-grid x-gap="24" :cols="2">
<n-gi>
<n-form-item label="所属部门" path="deptId">
<n-tree-select
key-field="id"
:options="deptTreeOptions"
:default-value="formParams.deptId"
@update:value="handleUpdateDeptValue"
clearable
filterable
default-expand-all
/>
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="绑定角色" path="roleId">
<n-tree-select
key-field="id"
:options="roleTreeOptions"
:default-value="formParams.roleId"
@update:value="handleUpdateRoleValue"
clearable
filterable
default-expand-all
/>
</n-form-item>
</n-gi>
</n-grid>
<n-divider title-placement="left">填写更多信息(可选)</n-divider>
<n-grid x-gap="24" :cols="2">
<n-gi>
<n-form-item label="姓名" path="realName">
<n-input placeholder="请输入姓名" v-model:value="formParams.realName" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="绑定岗位" path="postIds">
<n-select
:default-value="formParams.postIds"
:options="dict.getOptionUnRef('adminPostOption')"
@update:value="handleUpdatePostValue"
multiple
clearable
filterable
/>
</n-form-item>
</n-gi>
</n-grid>
<n-grid x-gap="24" :cols="2">
<n-gi>
<n-form-item label="手机号" path="mobile">
<n-input placeholder="请输入" v-model:value="formParams.mobile" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="邮箱" path="email">
<n-input placeholder="请输入" v-model:value="formParams.email" />
</n-form-item>
</n-gi>
</n-grid>
<n-grid x-gap="24" :cols="2">
<n-gi>
<n-form-item label="性别" path="sex">
<n-radio-group v-model:value="formParams.sex" name="sex">
<n-radio-button
v-for="status in dict.getOptionUnRef('sys_user_sex')"
:key="status.value"
:value="status.value"
:label="status.label"
/>
</n-radio-group>
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="状态" path="status">
<n-radio-group v-model:value="formParams.status" name="status">
<n-radio-button
v-for="status in dict.getOptionUnRef('sys_normal_disable')"
:key="status.value"
:value="status.value"
:label="status.label"
/>
</n-radio-group>
</n-form-item>
</n-gi>
</n-grid>
<n-form-item label="备注" path="remark">
<n-input type="textarea" placeholder="请输入备注" v-model:value="formParams.remark" />
</n-form-item>
</n-form>
<template #action>
<n-space>
<n-button @click="() => (showModal = false)">取消</n-button>
<n-button type="info" :loading="formBtnLoading" @click="confirmForm">确定</n-button>
</n-space>
</template>
</n-modal>
<AddBalance
@reload-table="reloadTable"
@update-show-modal="updateBalanceShowModal"
:showModal="showBalanceModal"
:formParams="formParams"
/>
<AddIntegral
@reload-table="reloadTable"
@update-show-modal="updateIntegralShowModal"
:showModal="showIntegralModal"
:formParams="formParams"
/>
<n-modal v-model:show="showQrModal" :show-icon="false" preset="dialog" title="邀请注册二维码">
<n-form class="py-4">
<div class="text-center">
<qrcode-vue :value="qrParams.qrUrl" :size="220" class="canvas" style="margin: 0 auto" />
</div>
</n-form>
<template #action>
<n-space>
<n-button @click="() => (showQrModal = false)">关闭</n-button>
</n-space>
</template>
</n-modal>
</div>
</template>
<script lang="ts" setup>
import { h, reactive, ref, computed } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import { ActionItem, BasicTable, TableAction } from '@/components/Table';
import { BasicForm } from '@/components/Form/index';
import { Delete, Edit, List, ResetPwd } from '@/api/org/user';
import { columns } from './columns';
import { PlusOutlined, DeleteOutlined } from '@vicons/antd';
import { QrCodeOutline } from '@vicons/ionicons5';
import { adaModalWidth, adaTableScrollX } from '@/utils/hotgo';
import { getRandomString } from '@/utils/charset';
import { cloneDeep } from 'lodash-es';
import QrcodeVue from 'qrcode.vue';
import AddBalance from './addBalance.vue';
import AddIntegral from './addIntegral.vue';
import {
addNewState,
addState,
register,
defaultState,
deptTreeOptions,
roleTreeOptions,
} from './model';
import { usePermission } from '@/hooks/web/usePermission';
import { useUserStore } from '@/store/modules/user';
import { LoginRoute } from '@/router';
import { getNowUrl } from '@/utils/urlUtils';
import { useDictStore } from '@/store/modules/dict';
interface Props {
type?: string;
}
const props = withDefaults(defineProps<Props>(), {
type: '-1',
});
const rules = {
username: {
required: true,
trigger: ['blur', 'input'],
message: '请输入用户名',
},
};
const dict = useDictStore();
const { hasPermission } = usePermission();
const userStore = useUserStore();
const showIntegralModal = ref(false);
const showBalanceModal = ref(false);
const message = useMessage();
const actionRef = ref();
const dialog = useDialog();
const showModal = ref(false);
const formBtnLoading = ref(false);
const searchFormRef = ref<any>({});
const formRef = ref<any>({});
const batchDeleteDisabled = ref(true);
const checkedIds = ref([]);
const formParams = ref<any>();
const showQrModal = ref(false);
const qrParams = ref({
name: '',
qrUrl: '',
});
const dialogWidth = computed(() => {
return adaModalWidth();
});
const actionColumn = reactive({
width: 200,
title: '操作',
key: 'action',
fixed: 'right',
render(record) {
const downActions = getDropDownActions(record);
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
ifShow: () => {
return record.id !== 1;
},
auth: ['/member/edit'],
},
{
label: '删除',
onClick: handleDelete.bind(null, record),
ifShow: () => {
return record.id !== 1;
},
auth: ['/member/delete'],
},
],
dropDownActions: downActions,
select: (key) => {
if (key === 0) {
return handleResetPwd(record);
}
if (key === 100) {
return handleAddBalance(record);
}
if (key === 101) {
return handleAddIntegral(record);
}
if (key === 102) {
if (userStore.loginConfig?.loginRegisterSwitch !== 1) {
message.error('管理员暂未开启此功能');
return;
}
return handleInviteQR(record.inviteCode);
}
},
});
},
});
const scrollX = computed(() => {
return adaTableScrollX(columns, actionColumn.width);
});
function getDropDownActions(record: Recordable): ActionItem[] {
if (record.id === 1) {
return [];
}
let list = [
{
label: '重置密码',
key: 0,
},
{
label: '变更余额',
key: 100,
},
{
label: '变更积分',
key: 101,
},
];
if (userStore.loginConfig?.loginRegisterSwitch === 1) {
list.push({
label: 'TA的邀请码',
key: 102,
});
}
return list;
}
function addTable() {
showModal.value = true;
formParams.value = cloneDeep(defaultState);
}
const loadDataTable = async (res) => {
return await List({ ...res, ...searchFormRef.value?.formModel, ...{ roleId: props.type } });
};
function onCheckedRow(rowKeys) {
batchDeleteDisabled.value = rowKeys.length <= 0;
checkedIds.value = rowKeys;
}
function reloadTable() {
actionRef.value.reload();
}
function confirmForm(e) {
e.preventDefault();
formBtnLoading.value = true;
formRef.value.validate((errors) => {
if (!errors) {
Edit(formParams.value).then((_res) => {
message.success('操作成功');
setTimeout(() => {
showModal.value = false;
reloadTable();
});
});
} else {
message.error('请填写完整信息');
}
formBtnLoading.value = false;
});
}
function handleEdit(record: Recordable) {
showModal.value = true;
formParams.value = cloneDeep(record);
}
function handleResetPwd(record: Recordable) {
record.password = getRandomString(12);
dialog.warning({
title: '警告',
content: '你确定要重置密码?\r\n重置成功后密码为:' + record.password + '\r\n 请先保存',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
ResetPwd(record).then((_res) => {
message.success('操作成功');
reloadTable();
});
},
});
}
function handleDelete(record: Recordable) {
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
Delete(record).then((_res) => {
message.success('操作成功');
reloadTable();
});
},
});
}
function batchDelete() {
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
Delete({ id: checkedIds.value }).then((_res) => {
message.success('操作成功');
reloadTable();
});
},
});
}
function handleSubmit(_values: Recordable) {
reloadTable();
}
function handleReset(_values: Recordable) {
reloadTable();
}
function handleUpdateDeptValue(value) {
formParams.value.deptId = Number(value);
}
function handleUpdateRoleValue(value) {
formParams.value.roleId = Number(value);
}
function handleUpdatePostValue(value) {
formParams.value.postIds = value;
}
function updateBalanceShowModal(value) {
showBalanceModal.value = value;
}
function handleAddBalance(record: Recordable) {
showBalanceModal.value = true;
formParams.value = addNewState(record as addState);
}
function updateIntegralShowModal(value) {
showIntegralModal.value = value;
}
function handleAddIntegral(record: Recordable) {
showIntegralModal.value = true;
formParams.value = addNewState(record as addState);
}
function handleInviteQR(code: any) {
const domain = getNowUrl() + '#';
qrParams.value.qrUrl = domain + LoginRoute.path + '?scope=register&inviteCode=' + code;
showQrModal.value = true;
}
</script>
<style lang="less" scoped></style>
+237
View File
@@ -0,0 +1,237 @@
import { cloneDeep } from 'lodash-es';
import { ref } from 'vue';
import { getDeptOption } from '@/api/org/dept';
import { getRoleOption } from '@/api/system/role';
import { FormSchema, useForm } from '@/components/Form';
import { defRangeShortcuts } from '@/utils/dateUtil';
import { useDictStore, Option } from '@/store/modules/dict';
// 增加余额/积分.
const dict = useDictStore();
export interface addState {
id: number;
username: string;
realName: string;
integral: number;
balance: number;
operateMode: number;
num: number | null;
}
export const addDefaultState = {
id: 0,
realName: '',
username: '',
integral: 0,
balance: 0,
operateMode: 1,
num: null,
};
export function addNewState(state: addState | null): addState {
if (state !== null) {
return cloneDeep(state);
}
return cloneDeep(addDefaultState);
}
export const operateModes = [
{
value: 1,
label: '加款',
},
{
value: 2,
label: '扣款',
},
];
export const addRules = {};
// 用户列表.
export const defaultState = {
id: 0,
roleId: null,
realName: '',
username: '',
password: '',
deptId: null,
postIds: null,
mobile: '',
email: '',
sex: 1,
leader: '',
phone: '',
sort: 0,
status: 1,
createdAt: '',
updatedAt: '',
};
export interface State {
id: number;
roleId: number | null;
realName: string;
username: string;
password: string;
deptId: number | null;
postIds: any;
mobile: string;
email: string;
sex: number;
leader: string;
phone: string;
sort: number;
status: number;
createdAt: string;
updatedAt: string;
}
const schemas: FormSchema[] = [
{
field: 'username',
component: 'NInput',
label: '用户名',
componentProps: {
placeholder: '请输入用户名',
onUpdateValue: (e: any) => {
console.log(e);
},
},
rules: [{ message: '请输入用户名', trigger: ['blur'] }],
},
{
field: 'realName',
component: 'NInput',
label: '姓名',
componentProps: {
placeholder: '请输入姓名',
showButton: false,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'mobile',
component: 'NInputNumber',
label: '手机号',
componentProps: {
placeholder: '请输入手机号码',
showButton: false,
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'email',
component: 'NInput',
label: '邮箱',
componentProps: {
placeholder: '请输入邮箱地址',
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'id',
component: 'NInput',
label: '用户ID',
componentProps: {
placeholder: '请输入用户ID',
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'pid',
component: 'NInput',
label: '上级ID',
componentProps: {
placeholder: '请输入上级用户ID',
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'status',
component: 'NSelect',
label: '状态',
defaultValue: null,
componentProps: {
placeholder: '请选择类型',
options: dict.getOption('sys_normal_disable'),
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'created_at',
component: 'NDatePicker',
label: '创建时间',
componentProps: {
type: 'datetimerange',
clearable: true,
shortcuts: defRangeShortcuts(),
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
];
export const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
export const deptTreeOptions = ref([]);
export const roleTreeOptions = ref([]);
export async function loadOptions() {
dict.loadOptions(['adminPostOption', 'sys_user_sex', 'sys_normal_disable']);
getDeptOption().then((res) => {
if (res.list) {
deptTreeOptions.value = res.list;
}
});
getRoleOption().then((res) => {
if (res.list) {
roleTreeOptions.value = res.list;
registerRoleTabsOption();
}
});
}
// 将角色Tabs选项注册到字典
function registerRoleTabsOption() {
const items = [{ id: -1, name: '全部' }];
treeDataToCompressed(items, roleTreeOptions.value);
const roleTabs: Option[] = [];
for (const item of items) {
roleTabs.push(dict.genOption(item.id, item.name));
}
dict.setOption('roleTabs', roleTabs);
}
function treeDataToCompressed(items: any[], source: any) {
for (const i in source) {
items.push(source[i]);
source[i].children && source[i].children.length > 0
? treeDataToCompressed(items, source[i].children)
: ''; // 子级递归
}
return items;
}
+46
View File
@@ -0,0 +1,46 @@
<template>
<div>
<n-card :bordered="false" class="proCard" title="后台用户">
<n-tabs
type="card"
class="card-tabs"
:value="defaultTab"
animated
@before-leave="handleBeforeLeave"
>
<n-tab-pane
:name="item.key.toString()"
:tab="item.label"
v-for="item in dict.getOptionUnRef('roleTabs')"
:key="item.key"
>
<List :type="defaultTab" />
</n-tab-pane>
</n-tabs>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import List from './list.vue';
import { useRouter } from 'vue-router';
import { loadOptions } from './model';
import { useDictStore } from '@/store/modules/dict';
const dict = useDictStore();
const router = useRouter();
const defaultTab = ref('-1');
onMounted(() => {
if (router.currentRoute.value.query?.type) {
defaultTab.value = router.currentRoute.value.query.type as string;
}
loadOptions();
});
function handleBeforeLeave(tabName: string): boolean | Promise<boolean> {
defaultTab.value = tabName;
return true;
}
</script>