This commit is contained in:
孟帅
2024-04-22 23:08:40 +08:00
parent 82483bd7b9
commit e144b12580
445 changed files with 17457 additions and 6708 deletions

View File

@@ -150,7 +150,7 @@
</template>
<script lang="ts" setup>
import { onMounted, ref, computed, watch } from 'vue';
import { ref, computed, watch } from 'vue';
import { rules, options, State, newState } from './model';
import { Edit, MaxSort } from '@/api/addons/hgexample/table';
import { useMessage } from 'naive-ui';
@@ -190,8 +190,10 @@
const message = useMessage();
const formRef = ref<any>({});
const dialogWidth = ref('75%');
const formBtnLoading = ref(false);
const dialogWidth = computed(() => {
return adaModalWidth();
});
function confirmForm(e) {
e.preventDefault();
@@ -230,10 +232,6 @@
}
}
);
onMounted(async () => {
adaModalWidth(dialogWidth);
});
</script>
<style lang="less"></style>

View File

@@ -27,7 +27,7 @@
:actionColumn="actionColumn"
:checked-row-keys="checkedIds"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1090"
:scroll-x="scrollX"
:resizeHeightOffset="-10000"
size="small"
@update:sorter="handleUpdateSorter"
@@ -75,7 +75,7 @@
</template>
<script lang="ts" setup>
import { h, reactive, ref } from 'vue';
import { computed, h, reactive, ref } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, useForm } from '@/components/Form/index';
@@ -84,7 +84,7 @@
import { State, columns, schemas, options, newState } from './model';
import { DeleteOutlined, PlusOutlined, ExportOutlined } from '@vicons/antd';
import { useRouter } from 'vue-router';
import { getOptionLabel } from '@/utils/hotgo';
import { adaTableScrollX, getOptionLabel } from '@/utils/hotgo';
import Edit from './edit.vue';
const router = useRouter();
@@ -102,7 +102,7 @@
width: 300,
title: '操作',
key: 'action',
// fixed: 'right',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
@@ -161,6 +161,10 @@
},
});
const scrollX = computed(() => {
return adaTableScrollX(columns, actionColumn.width);
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,

View File

@@ -0,0 +1,76 @@
<template>
<n-alert :show-icon="false" title="说明">
<n-p
>这里主要演示多租户业务下不同用户身份如何在同一页面下展示不同的表格功能和字段数据以及添加/编辑购买订单时服务端如何自动维护多租户关系</n-p
>
<n-p style="font-weight: 600">不同身份的测试账号</n-p>
<n-table :bordered="false" :single-line="false" size="small">
<thead>
<tr>
<th class="table-center">身份</th>
<th class="table-center">ID</th>
<th class="table-center">账号</th>
<th class="table-center">密码</th>
<th>身份描述</th>
</tr>
</thead>
<tbody>
<tr v-for="account in accounts" :key="account.id">
<td class="table-center">{{ account.type }}</td>
<td class="table-center">{{ account.id }}</td>
<td class="table-center">{{ account.username }}</td>
<td class="table-center">{{ account.password }}</td>
<td>{{ account.dc }}</td>
</tr>
</tbody>
</n-table>
</n-alert>
</template>
<script setup lang="ts">
interface Account {
type: string;
id: number;
username: string;
password: string;
dc: string;
}
const accounts: Account[] = [
{
type: '公司',
id: 1,
username: 'admin',
password: '123456',
dc: '可见全部数据。管理整个平台,包括商户和用户账户',
},
{
type: '租户',
id: 8,
username: 'ameng',
password: '123456',
dc: '可见自己下面的商户和用户数据。多租户系统中顶层实体,有自己的多个商户、用户、产品、订单等',
},
{
type: '商户',
id: 11,
username: 'abai',
password: '123456',
dc: '可见自己下面的用户数据。受租户的监管和管理,可以独立经营的实体,提供产品或服务,管理自己的业务,包括库存管理、订单处理、结算等',
},
{
type: '用户',
id: 12,
username: 'asong',
password: '123456',
dc: '只能看到自己数据。真正购买产品或享受服务的人,与商户互动,管理个人信息等个性化功能',
},
];
</script>
<style scoped lang="less">
.table-center {
text-align: center;
min-width: 80px;
}
</style>

View File

@@ -0,0 +1,163 @@
<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" v-if="userStore.isCompanyDept">
<n-form-item label="租户ID" path="tenantId">
<n-input placeholder="请输入租户ID" v-model:value="formValue.tenantId" />
</n-form-item>
</n-gi>
<n-gi span="1" v-if="userStore.isCompanyDept || userStore.isTenantDept">
<n-form-item label="商户ID" path="merchantId">
<n-input placeholder="请输入商户ID" v-model:value="formValue.merchantId" />
</n-form-item>
</n-gi>
<n-gi
span="1"
v-if="userStore.isCompanyDept || userStore.isTenantDept || userStore.isMerchantDept"
>
<n-form-item label="用户ID" path="userId">
<n-input placeholder="请输入用户ID" v-model:value="formValue.userId" />
</n-form-item>
</n-gi>
<n-gi span="1">
<n-form-item label="购买产品" path="productName">
<n-input placeholder="请输入购买产品" v-model:value="formValue.productName" />
</n-form-item>
</n-gi>
<n-gi span="1">
<n-form-item label="关联订单号" path="orderSn">
<n-input placeholder="请输入关联订单号" v-model:value="formValue.orderSn" />
</n-form-item>
</n-gi>
<n-gi span="1">
<n-form-item label="充值金额" path="money">
<n-input-group>
<n-input-number
:min="1"
:show-button="false"
style="width: 100%"
placeholder="请输入充值金额"
v-model:value="formValue.money"
/>
<n-input-group-label></n-input-group-label>
</n-input-group>
</n-form-item>
</n-gi>
<n-gi span="1">
<n-form-item label="备注" path="remark">
<n-input placeholder="请输入备注" v-model:value="formValue.remark" />
</n-form-item>
</n-gi>
<n-gi span="1">
<n-form-item label="支付状态" path="status">
<n-select v-model:value="formValue.status" :options="options.payStatus" />
</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 } from '@/api/addons/hgexample/tenantOrder';
import { options, State, newState, rules } from './model';
import { useProjectSettingStore } from '@/store/modules/projectSetting';
import { useMessage } from 'naive-ui';
import { adaModalWidth } from '@/utils/hotgo';
import { useUserStore } from '@/store/modules/user';
const emit = defineEmits(['reloadTable']);
const message = useMessage();
const settingStore = useProjectSettingStore();
const userStore = useUserStore();
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(840);
});
function openModal(state: State) {
showModal.value = true;
// 新增
if (!state || state.id < 1) {
formValue.value = newState(state);
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>

View File

@@ -0,0 +1,146 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="多租户功能演示">
<Alert />
</n-card>
</div>
<n-card :bordered="false" class="proCard">
<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"
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
:actionColumn="actionColumn"
:scroll-x="scrollX"
:resizeHeightOffset="-10000"
>
<template #tableTitle>
<n-button
type="primary"
@click="addTable"
class="min-left-space"
v-if="hasPermission(['/hgexample/tenantOrder/edit'])"
>
<template #icon>
<n-icon>
<PlusOutlined />
</n-icon>
</template>
添加购买订单
</n-button>
</template>
</BasicTable>
</n-card>
<Edit ref="editRef" @reloadTable="reloadTable" />
</div>
</template>
<script lang="ts" setup>
import { h, reactive, ref, computed, 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 { List, Delete } from '@/api/addons/hgexample/tenantOrder';
import { PlusOutlined } from '@vicons/antd';
import { columns, schemas, loadOptions } from './model';
import { adaTableScrollX } from '@/utils/hotgo';
import Edit from './edit.vue';
import Alert from './alert.vue';
const dialog = useDialog();
const message = useMessage();
const { hasPermission } = usePermission();
const actionRef = ref();
const searchFormRef = ref<any>({});
const editRef = ref();
const actionColumn = reactive({
width: 144,
title: '操作',
key: 'action',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
auth: ['/hgexample/tenantOrder/edit'],
},
{
label: '删除',
onClick: handleDelete.bind(null, record),
auth: ['/hgexample/tenantOrder/delete'],
},
],
});
},
});
const scrollX = computed(() => {
return adaTableScrollX(columns, actionColumn.width);
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
// 加载表格数据
const loadDataTable = async (res) => {
return await List({ ...searchFormRef.value?.formModel, ...res });
};
// 重新加载表格数据
function reloadTable() {
actionRef.value?.reload();
}
// 添加数据
function addTable() {
editRef.value.openModal(null);
}
// 编辑数据
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();
});
},
});
}
onMounted(() => {
loadOptions();
});
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,243 @@
import { h, ref } from 'vue';
import { NTag } from 'naive-ui';
import { cloneDeep } from 'lodash-es';
import { FormSchema } from '@/components/Form';
import { Dicts } from '@/api/dict/dict';
import { isNullObject } from '@/utils/is';
import { defRangeShortcuts } from '@/utils/dateUtil';
import { Option, getOptionLabel, getOptionTag } from '@/utils/hotgo';
import { useUserStore } from '@/store/modules/user';
const userStore = useUserStore();
export class State {
public id = 0; // 主键
public tenantId = null; // 租户ID
public merchantId = null; // 商户ID
public userId = null; // 用户ID
public productName = ''; // 购买产品
public orderSn = ''; // 关联订单号
public money = null; // 充值金额
public remark = ''; // 备注
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 = {
money: {
required: true,
trigger: ['blur', 'input'],
type: 'number',
message: '请输入充值金额',
},
};
// 表格搜索表单
export const schemas = ref<FormSchema[]>([
{
field: 'tenantId',
component: 'NInput',
label: '租户ID',
componentProps: {
placeholder: '请输入租户ID',
onUpdateValue: (e: any) => {
console.log(e);
},
},
ifShow: () => {
return userStore.isCompanyDept;
},
},
{
field: 'merchantId',
component: 'NInput',
label: '商户ID',
componentProps: {
placeholder: '请输入商户ID',
onUpdateValue: (e: any) => {
console.log(e);
},
},
ifShow: () => {
return userStore.isCompanyDept || userStore.isTenantDept;
},
},
{
field: 'userId',
component: 'NInput',
label: '用户ID',
componentProps: {
placeholder: '请输入用户ID',
onUpdateValue: (e: any) => {
console.log(e);
},
},
ifShow: () => {
return userStore.isCompanyDept || userStore.isTenantDept || userStore.isMerchantDept;
},
},
{
field: 'orderSn',
component: 'NInput',
label: '订单号',
componentProps: {
placeholder: '请输入订单号',
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'status',
component: 'NSelect',
label: '订单状态',
defaultValue: null,
componentProps: {
placeholder: '请选择订单状态',
options: [],
onUpdateValue: (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 columns = [
{
title: '订单ID',
key: 'id',
align: 'left',
width: 100,
},
{
title: '租户ID',
key: 'tenantId',
align: 'left',
width: 100,
ifShow: () => {
return userStore.isCompanyDept;
},
},
{
title: '商户ID',
key: 'merchantId',
align: 'left',
width: 100,
ifShow: () => {
return userStore.isCompanyDept || userStore.isTenantDept;
},
},
{
title: '用户ID',
key: 'userId',
align: 'left',
width: 100,
ifShow: () => {
return userStore.isCompanyDept || userStore.isTenantDept || userStore.isMerchantDept;
},
},
{
title: '购买产品',
key: 'productName',
align: 'left',
width: 150,
},
{
title: '订单号',
key: 'orderSn',
align: 'left',
width: 200,
},
{
title: '充值金额',
key: 'money',
align: 'left',
width: 100,
render(row) {
return row.money + ' 元';
},
},
{
title: '订单状态',
key: 'status',
align: 'left',
width: 100,
render(row) {
if (isNullObject(row.status)) {
return ``;
}
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: getOptionTag(options.value.payStatus, row.status),
bordered: false,
},
{
default: () => getOptionLabel(options.value.payStatus, row.status),
}
);
},
},
{
title: '创建时间',
key: 'createdAt',
align: 'left',
width: 180,
},
];
// 字典数据选项
export const options = ref({
payStatus: [] as Option[],
});
// 加载字典数据选项
export function loadOptions() {
Dicts({
types: ['payStatus'],
}).then((res) => {
options.value = res;
for (const item of schemas.value) {
switch (item.field) {
case 'status':
item.componentProps.options = options.value.payStatus;
break;
}
}
});
}

View File

@@ -200,8 +200,10 @@
const message = useMessage();
const formRef = ref<any>({});
const dialogWidth = ref('75%');
const formBtnLoading = ref(false);
const dialogWidth = computed(() => {
return adaModalWidth();
});
function confirmForm(e) {
e.preventDefault();
@@ -240,10 +242,6 @@
}
}
);
onMounted(async () => {
adaModalWidth(dialogWidth);
});
</script>
<style lang="less"></style>

View File

@@ -130,7 +130,6 @@
SearchOutlined,
DeleteOutlined,
} from '@vicons/antd';
import { getTreeItem } from '@/utils';
import List from './list.vue';
import { Delete, Select } from '@/api/addons/hgexample/treeTable';
import Edit from './edit.vue';
@@ -158,9 +157,9 @@
showModal.value = true;
}
function selectedTree(keys) {
function selectedTree(keys, opts) {
if (keys.length) {
const treeItem = getTreeItem(unref(treeData), keys[0]);
const treeItem = opts[0];
treeItemKey.value = keys;
treeItemTitle.value = treeItem.label;
formParams.value = newState(treeItem);

View File

@@ -30,7 +30,7 @@
:actionColumn="actionColumn"
:checked-row-keys="checkedIds"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1090"
:scroll-x="scrollX"
:resizeHeightOffset="-10000"
size="small"
@update:sorter="handleUpdateSorter"
@@ -81,7 +81,7 @@
</template>
<script lang="ts" setup>
import { h, reactive, ref, watch } from 'vue';
import { computed, h, reactive, ref, watch } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, useForm } from '@/components/Form/index';
@@ -89,7 +89,7 @@
import { Delete, List, Status, Export } from '@/api/addons/hgexample/treeTable';
import { State, columns, schemas, options, newState } from './model';
import { DeleteOutlined, PlusOutlined, ExportOutlined } from '@vicons/antd';
import { getOptionLabel } from '@/utils/hotgo';
import { adaTableScrollX, getOptionLabel } from '@/utils/hotgo';
import Edit from './edit.vue';
interface Props {
@@ -111,10 +111,10 @@
const pid = ref(0);
const actionColumn = reactive({
width: 300,
width: 200,
title: '操作',
key: 'action',
// fixed: 'right',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
@@ -146,6 +146,10 @@
},
});
const scrollX = computed(() => {
return adaTableScrollX(columns, actionColumn.width);
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,

View File

@@ -195,7 +195,7 @@
</template>
<script lang="ts" setup>
import { h, onMounted, reactive, ref } from 'vue';
import { computed, h, onMounted, reactive, ref } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
@@ -218,12 +218,12 @@
renderLabel,
renderMultipleSelectTag,
} from '@/enums/systemMessageEnum';
import { adaModalWidth, getOptionLabel, renderTag } from '@/utils/hotgo';
import { adaModalWidth, getOptionLabel } from '@/utils/hotgo';
import { renderTag } from '@/utils';
import Editor from '@/components/Editor/editor.vue';
import { cloneDeep } from 'lodash-es';
import { GetMemberOption } from '@/api/org/user';
import { usePermission } from '@/hooks/web/usePermission';
const { hasPermission } = usePermission();
const rules = {
title: {
@@ -286,6 +286,7 @@
},
];
const { hasPermission } = usePermission();
const message = useMessage();
const actionRef = ref();
const dialog = useDialog();
@@ -295,8 +296,10 @@
const formRef = ref<any>({});
const batchDeleteDisabled = ref(true);
const checkedIds = ref([]);
const dialogWidth = ref('75%');
const options = ref<personOption[]>();
const dialogWidth = computed(() => {
return adaModalWidth();
});
const resetFormParams = {
id: 0,
@@ -312,7 +315,7 @@
let formParams = ref<any>(cloneDeep(resetFormParams));
const actionColumn = reactive({
width: 180,
width: 200,
title: '操作',
key: 'action',
fixed: 'right',
@@ -440,9 +443,6 @@
reloadTable();
});
},
onNegativeClick: () => {
// message.error('取消');
},
});
}
@@ -458,9 +458,6 @@
reloadTable();
});
},
onNegativeClick: () => {
// message.error('取消');
},
});
}
@@ -486,7 +483,6 @@
}
onMounted(async () => {
adaModalWidth(dialogWidth);
await getMemberOption();
});
</script>

View File

@@ -79,7 +79,7 @@
</template>
<script lang="ts" setup>
import { onMounted, ref, computed, watch } from 'vue';
import { ref, computed, watch } from 'vue';
import { options, State, newState } from './model';
import { Edit, MaxSort, CheckProvincesUniqueId } from '@/api/apply/provinces';
import { FormItemRule, useMessage } from 'naive-ui';
@@ -140,8 +140,10 @@
const message = useMessage();
const formRef = ref<any>({});
const dialogWidth = ref('75%');
const formBtnLoading = ref(false);
const dialogWidth = computed(() => {
return adaModalWidth();
});
function confirmForm(e) {
e.preventDefault();
@@ -182,10 +184,6 @@
const res = await CheckProvincesUniqueId({ oldId: params.value.oldId, newId: newId });
return res.unique;
}
onMounted(async () => {
adaModalWidth(dialogWidth);
});
</script>
<style lang="less"></style>

View File

@@ -127,11 +127,11 @@
SearchOutlined,
DeleteOutlined,
} from '@vicons/antd';
import { getTreeItem } from '@/utils';
import List from './list.vue';
import { getProvincesTree, Delete } from '@/api/apply/provinces';
import Edit from './edit.vue';
import { newState } from './model';
const isUpdate = ref(false);
const showModal = ref(false);
const message = useMessage();
@@ -157,9 +157,9 @@
isUpdate.value = true;
}
function selectedTree(keys) {
function selectedTree(keys, opts) {
if (keys.length) {
const treeItem = getTreeItem(unref(treeData), keys[0]);
const treeItem = opts[0];
treeItemKey.value = keys;
treeItemTitle.value = treeItem.label;
formParams.value = newState(treeItem);

View File

@@ -26,7 +26,7 @@
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1090"
:scroll-x="scrollX"
:resizeHeightOffset="-10000"
>
<template #tableTitle>
@@ -54,7 +54,7 @@
</template>
<script lang="ts" setup>
import { h, reactive, ref, watch } from 'vue';
import { h, reactive, ref, watch, computed } from 'vue';
import { useMessage, useDialog } from 'naive-ui';
import { BasicColumn, BasicTable, TableAction } from '@/components/Table';
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
@@ -62,6 +62,8 @@
import { PlusOutlined } from '@vicons/antd';
import { getProvincesChildrenList, Delete } from '@/api/apply/provinces';
import Edit from './edit.vue';
import { adaTableScrollX } from '@/utils/hotgo';
const emit = defineEmits(['reloadTable']);
interface Props {
@@ -111,10 +113,10 @@
];
const actionColumn = reactive<BasicColumn>({
width: 220,
width: 150,
title: '操作',
key: 'action',
// fixed: 'right',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
@@ -132,6 +134,10 @@
},
});
const scrollX = computed(() => {
return adaTableScrollX(listColumns, actionColumn.width);
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:1 l:2 xl:2 2xl:2' },
labelWidth: 80,

View File

@@ -19,7 +19,8 @@
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
:scroll-x="1280"
:scroll-x="scrollX"
:resizeHeightOffset="-10000"
>
<template #tableTitle>
<n-button type="primary" @click="addTable" class="min-left-space">
@@ -56,7 +57,6 @@
</n-alert>
<n-form
:model="formParams"
:rules="rules"
ref="formRef"
label-placement="left"
:label-width="100"
@@ -150,7 +150,7 @@
</template>
<script lang="ts" setup>
import { h, reactive, ref } from 'vue';
import { computed, h, reactive, ref } from 'vue';
import { useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
@@ -162,6 +162,7 @@
import { useRouter } from 'vue-router';
import { getUserInfo } from '@/api/system/user';
import { getCashConfig } from '@/api/sys/config';
import { adaTableScrollX } from '@/utils/hotgo';
interface Props {
type?: string;
@@ -170,8 +171,8 @@
const props = withDefaults(defineProps<Props>(), {
type: '',
});
const router = useRouter();
const router = useRouter();
const params = ref<any>({
pageSize: 10,
title: '',
@@ -179,8 +180,6 @@
status: null,
});
const rules = {};
const estimated = ref(
'本次提现预计将在 ' +
timestampToTime(new Date().setTime(new Date().getTime() + 86400 * 4 * 1000) / 1000) +
@@ -276,6 +275,10 @@
},
});
const scrollX = computed(() => {
return adaTableScrollX(columns, actionColumn.width);
});
function setCash() {
router.push({
name: 'home_account',
@@ -334,9 +337,6 @@
reloadTable();
formParams.value = ref(resetFormParams);
});
})
.catch((_e: Error) => {
// message.error(e.message ?? '操作失败');
});
} else {
message.error('请填写完整信息');
@@ -366,9 +366,6 @@
reloadTable();
PaymentRef.value = ref(resetPaymentParams);
});
})
.catch((_e: Error) => {
// message.error(e.message ?? '操作失败');
});
} else {
message.error('请填写完整信息');

View File

@@ -19,7 +19,7 @@
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:scroll-x="1280"
:scroll-x="scrollX"
:resizeHeightOffset="-10000"
size="small"
>
@@ -44,7 +44,7 @@
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { computed, ref } from 'vue';
import { useMessage } from 'naive-ui';
import { BasicTable } from '@/components/Table';
import { BasicForm, useForm } from '@/components/Form/index';
@@ -52,6 +52,7 @@
import { List, Export } from '@/api/creditsLog';
import { columns, schemas } from './model';
import { ExportOutlined } from '@vicons/antd';
import { adaTableScrollX } from '@/utils/hotgo';
interface Props {
type?: string;
@@ -61,6 +62,10 @@
type: '',
});
const scrollX = computed(() => {
return adaTableScrollX(columns, 0);
});
const { hasPermission } = usePermission();
const actionRef = ref();
const message = useMessage();

View File

@@ -63,7 +63,7 @@
</template>
<script lang="ts" setup>
import { onMounted, ref, computed, watch } from 'vue';
import { ref, computed, watch } from 'vue';
import { rules, State, newState, options } from './model';
import { useMessage } from 'naive-ui';
import { adaModalWidth } from '@/utils/hotgo';
@@ -95,8 +95,10 @@
const params = ref<State>(props.formParams);
const message = useMessage();
const formRef = ref<any>({});
const dialogWidth = ref('75%');
const formBtnLoading = ref(false);
const dialogWidth = computed(() => {
return adaModalWidth();
});
function confirmForm(e) {
e.preventDefault();
@@ -117,10 +119,6 @@
});
}
onMounted(async () => {
adaModalWidth(dialogWidth);
});
function closeForm() {
isShowModal.value = false;
}

View File

@@ -46,18 +46,19 @@
</template>
<script lang="ts" setup>
import { onMounted, ref, computed, watch } from 'vue';
import { ref, computed, watch } from 'vue';
import { rules, State, newState } from './model';
import { useMessage } from 'naive-ui';
import { adaModalWidth } from '@/utils/hotgo';
import { ApplyRefund, View } from '@/api/order';
const emit = defineEmits(['reloadTable', 'updateShowModal']);
interface Props {
showModal: boolean;
formParams?: State;
}
const emit = defineEmits(['reloadTable', 'updateShowModal']);
const props = withDefaults(defineProps<Props>(), {
showModal: false,
formParams: () => {
@@ -78,8 +79,10 @@
const params = ref<State>(props.formParams);
const message = useMessage();
const formRef = ref<any>({});
const dialogWidth = ref('75%');
const formBtnLoading = ref(false);
const dialogWidth = computed(() => {
return adaModalWidth();
});
function confirmForm(e) {
e.preventDefault();
@@ -100,10 +103,6 @@
});
}
onMounted(async () => {
adaModalWidth(dialogWidth);
});
function closeForm() {
isShowModal.value = false;
}

View File

@@ -21,7 +21,7 @@
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1280"
:scroll-x="scrollX"
:resizeHeightOffset="-10000"
>
<template #tableTitle>
@@ -74,7 +74,7 @@
</template>
<script lang="ts" setup>
import { h, reactive, ref } from 'vue';
import { computed, h, reactive, ref } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, useForm } from '@/components/Form/index';
@@ -84,6 +84,8 @@
import { ExportOutlined, DeleteOutlined } from '@vicons/antd';
import ApplyRefund from './applyRefund.vue';
import AcceptRefund from './acceptRefund.vue';
import { adaTableScrollX } from '@/utils/hotgo';
interface Props {
type?: string;
}
@@ -142,6 +144,10 @@
},
});
const scrollX = computed(() => {
return adaTableScrollX(columns, actionColumn.width);
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,

View File

@@ -1,89 +1,104 @@
<template>
<div>
<n-spin :show="loading" description="请稍候...">
<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-modal
v-model:show="showModal"
:mask-closable="false"
:show-icon="false"
preset="dialog"
transform-origin="center"
:title="formValue.id > 0 ? '编辑CURD列表 #' + formValue.id : '添加CURD列表'"
: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"
ref="formRef"
:label-placement="settingStore.isMobile ? 'top' : 'left'"
:label-width="100"
class="py-4"
>
<n-form-item label="分类ID" path="categoryId">
<n-input-number placeholder="请输入分类ID" v-model:value="formValue.categoryId" />
</n-form-item>
<n-form-item label="标题" path="title">
<n-input placeholder="请输入标题" v-model:value="formValue.title" />
</n-form-item>
<n-form-item label="描述" path="description">
<n-input type="textarea" placeholder="描述" v-model:value="formValue.description" />
</n-form-item>
<n-form-item label="内容" path="content">
<Editor style="height: 450px" id="content" v-model:value="formValue.content" />
</n-form-item>
<n-form-item label="单图" path="image">
<UploadImage :maxNumber="1" v-model:value="formValue.image" />
</n-form-item>
<n-form-item label="附件" path="attachfile">
<UploadFile :maxNumber="1" v-model:value="formValue.attachfile" />
</n-form-item>
<n-form-item label="所在城市" path="cityId">
<CitySelector v-model:value="formValue.cityId" />
</n-form-item>
<n-form-item label="显示开关" path="switch">
<n-switch :unchecked-value="2" :checked-value="1" v-model:value="formValue.switch"
<n-grid cols="1 s:1 m:2 l:2 xl:2 2xl:2" responsive="screen">
<n-gi span="1">
<n-form-item label="标题" path="title">
<n-input placeholder="请输入标题" v-model:value="formValue.title" />
</n-form-item>
</n-gi>
<n-gi span="2">
<n-form-item label="描述" path="description">
<n-input type="textarea" placeholder="描述" v-model:value="formValue.description" />
</n-form-item>
</n-gi>
<n-gi span="2">
<n-form-item label="内容" path="content">
<Editor style="height: 450px" id="content" v-model:value="formValue.content" />
</n-form-item>
</n-gi>
<n-gi span="1">
<n-form-item label="单图" path="image">
<UploadImage :maxNumber="1" v-model:value="formValue.image" />
</n-form-item>
</n-gi>
<n-gi span="1">
<n-form-item label="附件" path="attachfile">
<UploadFile :maxNumber="1" v-model:value="formValue.attachfile" />
</n-form-item>
</n-gi>
<n-gi span="1">
<n-form-item label="所在城市" path="cityId">
<CitySelector v-model:value="formValue.cityId" />
</n-form-item>
</n-gi>
<n-gi span="1">
<n-form-item label="排序" path="sort">
<n-input-number placeholder="请输入排序" v-model:value="formValue.sort" />
</n-form-item>
</n-gi>
<n-gi span="1">
<n-form-item label="显示开关" path="switch">
<n-switch :unchecked-value="2" :checked-value="1" v-model:value="formValue.switch"
/>
</n-form-item>
<n-form-item label="排序" path="sort">
<n-input-number placeholder="请输入排序" v-model:value="formValue.sort" />
</n-form-item>
<n-form-item label="状态" path="status">
<n-select v-model:value="formValue.status" :options="options.sys_normal_disable" />
</n-form-item>
</n-form-item>
</n-gi>
<n-gi span="1">
<n-form-item label="状态" path="status">
<n-select v-model:value="formValue.status" :options="options.sys_normal_disable" />
</n-form-item>
</n-gi>
<n-gi span="1">
<n-form-item label="测试分类" path="categoryId">
<n-select v-model:value="formValue.categoryId" :options="options.testCategoryOption" />
</n-form-item>
</n-gi>
</n-grid>
</n-form>
</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>
</n-spin>
</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 } from 'vue';
import { Edit, MaxSort, View } from '@/api/curdDemo';
import { ref, computed } from 'vue';
import { Edit, View, MaxSort } from '@/api/curdDemo';
import { options, State, newState, rules } from './model';
import Editor from '@/components/Editor/editor.vue';
import UploadImage from '@/components/Upload/uploadImage.vue';
import UploadFile from '@/components/Upload/uploadFile.vue';
import CitySelector from '@/components/CitySelector/citySelector.vue';
import { rules, options, State, newState } from './model';
import { useProjectSettingStore } from '@/store/modules/projectSetting';
import { useMessage } from 'naive-ui';
import { adaModalWidth } from '@/utils/hotgo';
@@ -91,12 +106,43 @@
const emit = defineEmits(['reloadTable']);
const message = useMessage();
const settingStore = useProjectSettingStore();
const dialogWidth = ref('75%');
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(840);
});
function openModal(state: State) {
showModal.value = true;
// 新增
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();
@@ -106,7 +152,7 @@
Edit(formValue.value).then((_res) => {
message.success('操作成功');
setTimeout(() => {
showModal.value = false;
closeForm();
emit('reloadTable');
});
});
@@ -122,37 +168,9 @@
loading.value = false;
}
function openModal(state: State) {
adaModalWidth(dialogWidth);
showModal.value = true;
loading.value = true;
// 新增
if (!state || state.id < 1) {
formValue.value = newState(state);
MaxSort()
.then((res) => {
formValue.value.sort = res.sort;
})
.finally(() => {
loading.value = false;
});
return;
}
// 编辑
View({ id: state.id })
.then((res) => {
formValue.value = res;
})
.finally(() => {
loading.value = false;
});
}
defineExpose({
openModal,
});
</script>
<style lang="less"></style>
<style lang="less"></style>

View File

@@ -1,44 +1,19 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="生成演示">
<n-card :bordered="false" title="CURD列表">
<!-- 这是由系统生成的CURD表格你可以将此行注释改为表格的描述 -->
</n-card>
</div>
<n-card :bordered="false" class="proCard">
<BasicForm
@register="register"
@submit="reloadTable"
@reset="reloadTable"
@keyup.enter="reloadTable"
ref="searchFormRef"
>
<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
:openChecked="true"
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
:checked-row-keys="checkedIds"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1090"
:resizeHeightOffset="-10000"
size="small"
>
<BasicTable ref="actionRef" openChecked :columns="columns" :request="loadDataTable" :row-key="(row) => row.id" :actionColumn="actionColumn" :scroll-x="scrollX" :resizeHeightOffset="-10000" :checked-row-keys="checkedIds" @update:checked-row-keys="handleOnCheckedRow">
<template #tableTitle>
<n-button
type="primary"
@click="addTable"
class="min-left-space"
v-if="hasPermission(['/curdDemo/edit'])"
>
<n-button type="primary" @click="addTable" class="min-left-space" v-if="hasPermission(['/curdDemo/edit'])">
<template #icon>
<n-icon>
<PlusOutlined />
@@ -46,13 +21,7 @@
</template>
添加
</n-button>
<n-button
type="error"
@click="handleBatchDelete"
:disabled="batchDeleteDisabled"
class="min-left-space"
v-if="hasPermission(['/curdDemo/delete'])"
>
<n-button type="error" @click="handleBatchDelete" class="min-left-space" v-if="hasPermission(['/curdDemo/delete'])">
<template #icon>
<n-icon>
<DeleteOutlined />
@@ -60,12 +29,7 @@
</template>
批量删除
</n-button>
<n-button
type="primary"
@click="handleExport"
class="min-left-space"
v-if="hasPermission(['/curdDemo/export'])"
>
<n-button type="primary" @click="handleExport" class="min-left-space" v-if="hasPermission(['/curdDemo/export'])">
<template #icon>
<n-icon>
<ExportOutlined />
@@ -76,39 +40,36 @@
</template>
</BasicTable>
</n-card>
<Edit @reloadTable="reloadTable" ref="editRef" />
<View ref="viewRef" />
<Edit ref="editRef" @reloadTable="reloadTable" />
</div>
</template>
<script lang="ts" setup>
import { h, reactive, ref } from 'vue';
import { h, reactive, ref, computed, 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 { List, Export, Delete, Status } from '@/api/curdDemo';
import { columns, schemas, options } from './model';
import { List, Export, Delete } from '@/api/curdDemo';
import { PlusOutlined, ExportOutlined, DeleteOutlined } from '@vicons/antd';
import { getOptionLabel } from '@/utils/hotgo';
import { columns, schemas, loadOptions } from './model';
import { adaTableScrollX } from '@/utils/hotgo';
import Edit from './edit.vue';
import View from './view.vue';
const dialog = useDialog();
const message = useMessage();
const { hasPermission } = usePermission();
const actionRef = ref();
const searchFormRef = ref<any>({});
const viewRef = ref();
const editRef = ref();
const batchDeleteDisabled = ref(true);
const checkedIds = ref([]);
const actionColumn = reactive({
width: 300,
width: 144,
title: '操作',
key: 'action',
// fixed: 'right',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
@@ -118,75 +79,54 @@
onClick: handleEdit.bind(null, record),
auth: ['/curdDemo/edit'],
},
{
label: '禁用',
onClick: handleStatus.bind(null, record, 2),
ifShow: () => {
return record.status === 1;
},
auth: ['/curdDemo/status'],
},
{
label: '启用',
onClick: handleStatus.bind(null, record, 1),
ifShow: () => {
return record.status === 2;
},
auth: ['/curdDemo/status'],
},
{
label: '删除',
onClick: handleDelete.bind(null, record),
auth: ['/curdDemo/delete'],
},
],
dropDownActions: [
{
label: '查看详情',
key: 'view',
auth: ['/curdDemo/view'],
},
],
select: (key) => {
if (key === 'view') {
return handleView(record);
}
},
});
},
});
const scrollX = computed(() => {
return adaTableScrollX(columns, actionColumn.width);
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
// 加载表格数据
const loadDataTable = async (res) => {
return await List({ ...searchFormRef.value?.formModel, ...res });
};
function onCheckedRow(rowKeys) {
batchDeleteDisabled.value = rowKeys.length <= 0;
// 更新选中的行
function handleOnCheckedRow(rowKeys) {
checkedIds.value = rowKeys;
}
// 重新加载表格数据
function reloadTable() {
actionRef.value.reload();
actionRef.value?.reload();
}
// 添加数据
function addTable() {
editRef.value.openModal(null);
}
// 编辑数据
function handleEdit(record: Recordable) {
editRef.value.openModal(record);
}
function handleView(record: Recordable) {
viewRef.value.openModal(record);
}
// 单个删除
function handleDelete(record: Recordable) {
dialog.warning({
title: '警告',
@@ -202,7 +142,13 @@
});
}
// 批量删除
function handleBatchDelete() {
if (checkedIds.value.length < 1){
message.error('请至少选择一项要删除的数据');
return;
}
dialog.warning({
title: '警告',
content: '你确定要批量删除?',
@@ -210,7 +156,6 @@
negativeText: '取消',
onPositiveClick: () => {
Delete({ id: checkedIds.value }).then((_res) => {
batchDeleteDisabled.value = true;
checkedIds.value = [];
message.success('删除成功');
reloadTable();
@@ -219,19 +164,15 @@
});
}
// 导出
function handleExport() {
message.loading('正在导出列表...', { duration: 1200 });
Export(searchFormRef.value?.formModel);
}
function handleStatus(record: Recordable, status: number) {
Status({ id: record.id, status: status }).then((_res) => {
message.success('设为' + getOptionLabel(options.value.sys_normal_disable, status) + '成功');
setTimeout(() => {
reloadTable();
});
});
}
onMounted(() => {
loadOptions();
});
</script>
<style lang="less" scoped></style>
<style lang="less" scoped></style>

View File

@@ -1,42 +1,44 @@
import { h, ref } from 'vue';
import { NAvatar, NImage, NTag, NSwitch, NRate } from 'naive-ui';
import { NImage, NAvatar, NSwitch, NTag } from 'naive-ui';
import { cloneDeep } from 'lodash-es';
import { FormSchema } from '@/components/Form';
import { Dicts } from '@/api/dict/dict';
import { Switch } from '@/api/curdDemo';
import { isArray, isNullObject } from '@/utils/is';
import { isNullObject } from '@/utils/is';
import { getFileExt } from '@/utils/urlUtils';
import { defRangeShortcuts, defShortcuts, formatToDate } from '@/utils/dateUtil';
import { validate } from '@/utils/validateUtil';
import { getOptionLabel, getOptionTag, Option, Options, errorImg } from '@/utils/hotgo';
import { defRangeShortcuts } from '@/utils/dateUtil';
import { Option, errorImg, getOptionLabel, getOptionTag } from '@/utils/hotgo';
import { renderPopoverMemberSumma, MemberSumma } from '@/utils';
import { Switch } from '@/api/curdDemo';
import { usePermission } from '@/hooks/web/usePermission';
const { hasPermission } = usePermission();
const $message = window['$message'];
export class State {
public id = 0; // ID
public categoryId = 0; // 分类ID
public title = ''; // 标题
public description = ''; // 描述
public content = ''; // 内容
public image = ''; // 单图
public attachfile = ''; // 附件
public cityId = 0; // 所在城市
public switch = 2; // 显示开关
public cityId = null; // 所在城市
public sort = 0; // 排序
public switch = 2; // 显示开关
public status = 1; // 状态
public createdBy = 0; // 创建者
public updatedBy = 0; // 更新者
public createdBySumma?: null | MemberSumma = null; // 创建者摘要信息
public createdAt = ''; // 创建时间
public updatedBy = 0; // 更新者
public updatedBySumma?: null | MemberSumma = null; // 更新者摘要信息
public updatedAt = ''; // 修改时间
public deletedAt = ''; // 删除时间
public categoryId = null; // 测试分类
constructor(state?: Partial<State>) {
if (state) {
Object.assign(this, state);
}
}}
}
}
export function newState(state: State | Record<string, any> | null): State {
if (state !== null) {
@@ -48,21 +50,8 @@ export function newState(state: State | Record<string, any> | null): State {
return new State();
}
export interface IOptions extends Options {
sys_normal_disable: Option[];
};
export const options = ref<IOptions>({
sys_normal_disable: [],
});
// 表单验证规则
export const rules = {
categoryId: {
required: true,
trigger: ['blur', 'input'],
type: 'number',
message: '请输入分类ID',
},
title: {
required: true,
trigger: ['blur', 'input'],
@@ -87,12 +76,19 @@ export const rules = {
type: 'number',
message: '请输入排序',
},
categoryId: {
required: true,
trigger: ['blur', 'input'],
type: 'number',
message: '请输入测试分类',
},
};
// 表格搜索表单
export const schemas = ref<FormSchema[]>([
{
field: 'id',
component: 'NInputNumber',
component: 'NInput',
label: 'ID',
componentProps: {
placeholder: '请输入ID',
@@ -101,6 +97,28 @@ export const schemas = ref<FormSchema[]>([
},
},
},
{
field: 'title',
component: 'NInput',
label: '标题',
componentProps: {
placeholder: '请输入标题',
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'description',
component: 'NInput',
label: '描述',
componentProps: {
placeholder: '请输入描述',
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'status',
component: 'NSelect',
@@ -114,6 +132,17 @@ export const schemas = ref<FormSchema[]>([
},
},
},
{
field: 'createdBy',
component: 'NInput',
label: '创建者',
componentProps: {
placeholder: '请输入ID|用户名|姓名|手机号',
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'createdAt',
component: 'NDatePicker',
@@ -127,12 +156,25 @@ export const schemas = ref<FormSchema[]>([
},
},
},
{
field: 'categoryId',
component: 'NSelect',
label: '测试分类',
defaultValue: null,
componentProps: {
placeholder: '请选择测试分类',
options: [],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'testCategoryName',
component: 'NInput',
label: '分类名称',
label: '关联分类',
componentProps: {
placeholder: '请输入分类名称',
placeholder: '请输入关联分类',
onUpdateValue: (e: any) => {
console.log(e);
},
@@ -140,31 +182,37 @@ export const schemas = ref<FormSchema[]>([
},
]);
// 表格列
export const columns = [
{
title: 'ID',
key: 'id',
},
{
title: '分类ID',
key: 'categoryId',
align: 'left',
width: 50,
},
{
title: '标题',
key: 'title',
align: 'left',
width: 150,
},
{
title: '描述',
key: 'description',
align: 'left',
width: 300,
},
{
title: '单图',
key: 'image',
align: 'left',
width: 100,
render(row) {
return h(NImage, {
width: 32,
height: 32,
src: row.image,
fallbackSrc: errorImg,
onError: errorImg,
style: {
width: '32px',
@@ -178,6 +226,8 @@ export const columns = [
{
title: '附件',
key: 'attachfile',
align: 'left',
width: 100,
render(row) {
if (row.attachfile === '') {
return ``;
@@ -194,13 +244,16 @@ export const columns = [
},
},
{
title: '所在城市',
key: 'cityId',
title: '排序',
key: 'sort',
align: 'left',
width: 100,
},
{
title: '显示开关',
key: 'switch',
width: 100,
align: 'left',
width: 150,
render(row) {
return h(NSwitch, {
value: row.switch === 1,
@@ -217,13 +270,11 @@ export const columns = [
});
},
},
{
title: '排序',
key: 'sort',
},
{
title: '状态',
key: 'status',
align: 'left',
width: 100,
render(row) {
if (isNullObject(row.status)) {
return ``;
@@ -246,38 +297,86 @@ export const columns = [
{
title: '创建者',
key: 'createdBy',
},
{
title: '更新者',
key: 'updatedBy',
align: 'left',
width: 150,
render(row) {
return renderPopoverMemberSumma(row.createdBySumma);
},
},
{
title: '创建时间',
key: 'createdAt',
align: 'left',
width: 180,
},
{
title: '更新者',
key: 'updatedBy',
align: 'left',
width: 150,
render(row) {
return renderPopoverMemberSumma(row.updatedBySumma);
},
},
{
title: '修改时间',
key: 'updatedAt',
align: 'left',
width: 180,
},
{
title: '分类名称',
title: '测试分类',
key: 'categoryId',
align: 'left',
width: 100,
render(row) {
if (isNullObject(row.categoryId)) {
return ``;
}
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: getOptionTag(options.value.testCategoryOption, row.categoryId),
bordered: false,
},
{
default: () => getOptionLabel(options.value.testCategoryOption, row.categoryId),
}
);
},
},
{
title: '关联分类',
key: 'testCategoryName',
align: 'left',
width: 100,
},
];
async function loadOptions() {
options.value = await Dicts({
types: [
'sys_normal_disable',
],
});
for (const item of schemas.value) {
switch (item.field) {
case 'status':
item.componentProps.options = options.value.sys_normal_disable;
break;
}
}
}
// 字典数据选项
export const options = ref({
sys_normal_disable: [] as Option[],
testCategoryOption: [] as Option[],
});
await loadOptions();
// 加载字典数据选项
export function loadOptions() {
Dicts({
types: ['sys_normal_disable', 'testCategoryOption'],
}).then((res) => {
options.value = res;
for (const item of schemas.value) {
switch (item.field) {
case 'status':
item.componentProps.options = options.value.sys_normal_disable;
break;
case 'categoryId':
item.componentProps.options = options.value.testCategoryOption;
break;
}
}
});
}

View File

@@ -1,81 +1,78 @@
<template>
<div>
<n-spin :show="loading" description="请稍候...">
<n-drawer v-model:show="showModal" :width="dialogWidth">
<n-drawer-content>
<template #header> 生成演示详情 </template>
<template #footer>
<n-button @click="showModal = false"> 关闭 </n-button>
</template>
<n-drawer v-model:show="showModal" :width="dialogWidth">
<n-drawer-content title="CURD列表详情" closable>
<n-spin :show="loading" description="请稍候...">
<n-descriptions label-placement="left" class="py-2" column="1">
<n-descriptions-item>
<template #label>分类ID</template>
{{ formValue.categoryId }}
</n-descriptions-item>
<n-descriptions-item>
<template #label>标题</template>
{{ formValue.title }}
</n-descriptions-item>
<n-descriptions-item>
<template #label>描述</template>
<span v-html="formValue.description"></span></n-descriptions-item>
<n-descriptions-item>
<template #label>内容</template>
<span v-html="formValue.content"></span></n-descriptions-item>
<n-descriptions-item>
<template #label>单图</template>
<n-image style="margin-left: 10px; height: 100px; width: 100px" :src="formValue.image"
/></n-descriptions-item>
<n-descriptions-item>
<template #label>附件</template>
<div
class="upload-card"
v-show="formValue.attachfile !== ''"
@click="download(formValue.attachfile)"
>
<div class="upload-card-item" style="height: 100px; width: 100px">
<div class="upload-card-item-info">
<div class="img-box">
<n-avatar :style="fileAvatarCSS">{{ getFileExt(formValue.attachfile) }}</n-avatar>
<n-descriptions-item label="测试分类">
<n-tag :type="getOptionTag(options.testCategoryOption, formValue?.categoryId)" size="small" class="min-left-space">
{{ getOptionLabel(options.testCategoryOption, formValue?.categoryId) }}
</n-tag>
</n-descriptions-item>
<n-descriptions-item>
<template #label>
标题
</template>
{{ formValue.title }}
</n-descriptions-item>
<n-descriptions-item>
<template #label>
描述
</template>
<span v-html="formValue.description"></span>
</n-descriptions-item>
<n-descriptions-item>
<template #label>
内容
</template>
<span v-html="formValue.content"></span>
</n-descriptions-item>
<n-descriptions-item>
<template #label>
所在城市
</template>
{{ formValue.cityId }}
</n-descriptions-item>
<n-descriptions-item>
<template #label>
排序
</template>
{{ formValue.sort }}
</n-descriptions-item>
<n-descriptions-item>
<template #label>
单图
</template>
<n-image style="margin-left: 10px; height: 100px; width: 100px" :src="formValue.image"/>
</n-descriptions-item>
<n-descriptions-item label="显示开关">
<n-switch v-model:value="formValue.switch" :unchecked-value="2" :checked-value="1" :disabled="true"/>
</n-descriptions-item>
<n-descriptions-item>
<template #label>
附件
</template>
<div class="upload-card" v-show="formValue.attachfile !== ''" @click="download(formValue.attachfile)">
<div class="upload-card-item" style="height: 100px; width: 100px">
<div class="upload-card-item-info">
<div class="img-box">
<n-avatar :style="fileAvatarCSS">
{{ getFileExt(formValue.attachfile) }}
</n-avatar>
</div>
</div>
</div>
</div>
</div>
</div>
</n-descriptions-item>
<n-descriptions-item>
<template #label>所在城市</template>
{{ formValue.cityId }}
</n-descriptions-item>
<n-descriptions-item label="显示开关">
<n-switch v-model:value="formValue.switch" :unchecked-value="2" :checked-value="1" :disabled="true"
/></n-descriptions-item>
<n-descriptions-item>
<template #label>排序</template>
{{ formValue.sort }}
</n-descriptions-item>
<n-descriptions-item label="状态">
<n-tag
:type="getOptionTag(options.sys_normal_disable, formValue?.status)"
size="small"
class="min-left-space"
>{{ getOptionLabel(options.sys_normal_disable, formValue?.status) }}</n-tag
>
</n-descriptions-item>
</n-descriptions-item>
<n-descriptions-item label="状态">
<n-tag :type="getOptionTag(options.sys_normal_disable, formValue?.status)" size="small" class="min-left-space">
{{ getOptionLabel(options.sys_normal_disable, formValue?.status) }}
</n-tag>
</n-descriptions-item>
</n-descriptions>
</n-drawer-content>
</n-drawer>
</n-spin>
</n-spin>
</n-drawer-content>
</n-drawer>
</div>
</template>
@@ -88,10 +85,12 @@
import { getFileExt } from '@/utils/urlUtils';
const message = useMessage();
const dialogWidth = ref('75%');
const loading = ref(false);
const showModal = ref(false);
const formValue = ref(newState(null));
const dialogWidth = computed(() => {
return adaModalWidth(580);
});
const fileAvatarCSS = computed(() => {
return {
'--n-merged-size': `var(--n-avatar-size-override, 80px)`,
@@ -105,7 +104,6 @@
}
function openModal(state: State) {
adaModalWidth(dialogWidth, 580);
showModal.value = true;
loading.value = true;
View({ id: state.id })
@@ -122,4 +120,4 @@
});
</script>
<style lang="less" scoped></style>
<style lang="less" scoped></style>

View File

@@ -1,339 +1,24 @@
<template>
<div class="console">
<!--数据卡片-->
<n-grid cols="1 s:2 m:3 l:4 xl:4 2xl:4" responsive="screen" :x-gap="12" :y-gap="8">
<n-grid-item>
<NCard
title="卡板量"
:segmented="{ content: true, footer: true }"
size="small"
:bordered="false"
>
<template #header-extra>
<n-tag type="success"></n-tag>
</template>
<div class="py-1 px-1 flex justify-between">
<n-skeleton v-if="loading" :width="100" size="medium" />
<CountTo v-else :startVal="1" :endVal="visits.dayVisits" class="text-3xl" />
</div>
<div class="py-1 px-1 flex justify-between">
<div class="text-sn">
<n-skeleton v-if="loading" :width="100" size="medium" />
<template v-else>
日同比
<CountTo :startVal="1" suffix="%" :endVal="visits.rise" />
<n-icon size="12" color="#00ff6f">
<CaretUpOutlined />
</n-icon>
</template>
</div>
<div class="text-sn">
<n-skeleton v-if="loading" :width="100" size="medium" />
<template v-else>
周同比
<CountTo :startVal="1" suffix="%" :endVal="visits.decline" />
<n-icon size="12" color="#ffde66">
<CaretDownOutlined />
</n-icon>
</template>
</div>
</div>
<template #footer>
<div class="flex justify-between">
<n-skeleton v-if="loading" text :repeat="2" />
<template v-else>
<div class="text-sn"> 总卡板量 </div>
<div class="text-sn">
<CountTo :startVal="1" :endVal="visits.amount" />
</div>
</template>
</div>
</template>
</NCard>
</n-grid-item>
<n-grid-item>
<NCard
title="激活卡板"
:segmented="{ content: true, footer: true }"
size="small"
:bordered="false"
>
<template #header-extra>
<n-tag type="info"></n-tag>
</template>
<div class="py-1 px-1 flex justify-between">
<n-skeleton v-if="loading" :width="100" size="medium" />
<CountTo
v-else
prefix="¥"
:startVal="1"
:endVal="saleroom.weekSaleroom"
class="text-3xl"
/>
</div>
<div class="py-2 px-2 flex justify-between">
<div class="text-sn flex-1">
<n-progress
type="line"
:percentage="saleroom.degree"
:indicator-placement="'inside'"
processing
/>
</div>
</div>
<template #footer>
<div class="flex justify-between">
<n-skeleton v-if="loading" :width="100" size="medium" />
<template v-else>
<div class="text-sn"> 总激活卡板 </div>
<div class="text-sn">
<CountTo :startVal="1" :endVal="saleroom.amount" />
<!-- prefix="¥"-->
</div>
</template>
</div>
</template>
</NCard>
</n-grid-item>
<n-grid-item>
<NCard
title="代理商"
:segmented="{ content: true, footer: true }"
size="small"
:bordered="false"
>
<template #header-extra>
<n-tag type="warning"></n-tag>
</template>
<div class="py-1 px-1 flex justify-between">
<n-skeleton v-if="loading" :width="100" size="medium" />
<CountTo v-else :startVal="1" :endVal="orderLarge.weekLarge" class="text-3xl" />
</div>
<div class="py-1 px-1 flex justify-between">
<div class="text-sn">
<n-skeleton v-if="loading" :width="100" size="medium" />
<template v-else>
日同比
<CountTo :startVal="1" suffix="%" :endVal="orderLarge.rise" />
<n-icon size="12" color="#00ff6f">
<CaretUpOutlined />
</n-icon>
</template>
</div>
<div class="text-sn">
<n-skeleton v-if="loading" :width="100" size="medium" />
<template v-else>
周同比
<CountTo :startVal="1" suffix="%" :endVal="orderLarge.rise" />
<n-icon size="12" color="#ffde66">
<CaretDownOutlined />
</n-icon>
</template>
</div>
</div>
<template #footer>
<div class="flex justify-between">
<n-skeleton v-if="loading" :width="100" size="medium" />
<template v-else>
<div class="text-sn"> 总代理商量 </div>
<div class="text-sn">
<CountTo :startVal="1" suffix="%" :endVal="orderLarge.amount" />
</div>
</template>
</div>
</template>
</NCard>
</n-grid-item>
<n-grid-item>
<NCard
title="提现佣金"
:segmented="{ content: true, footer: true }"
size="small"
:bordered="false"
>
<template #header-extra>
<n-tag type="error"></n-tag>
</template>
<div class="py-1 px-1 flex justify-between">
<n-skeleton v-if="loading" :width="100" size="medium" />
<CountTo v-else prefix="¥" :startVal="1" :endVal="volume.weekLarge" class="text-3xl" />
</div>
<div class="py-1 px-1 flex justify-between">
<div class="text-sn">
<n-skeleton v-if="loading" :width="100" size="medium" />
<template v-else>
月同比
<CountTo :startVal="1" suffix="%" :endVal="volume.rise" />
<n-icon size="12" color="#00ff6f">
<CaretUpOutlined />
</n-icon>
</template>
</div>
<div class="text-sn">
<n-skeleton v-if="loading" :width="100" size="medium" />
<template v-else>
月同比
<CountTo :startVal="1" suffix="%" :endVal="volume.decline" />
<n-icon size="12" color="#ffde66">
<CaretDownOutlined />
</n-icon>
</template>
</div>
</div>
<template #footer>
<div class="flex justify-between">
<n-skeleton v-if="loading" :width="100" size="medium" />
<template v-else>
<div class="text-sn"> 总提现额 </div>
<div class="text-sn">
<CountTo prefix="¥" :startVal="1" :endVal="volume.amount" />
</div>
</template>
</div>
</template>
</NCard>
</n-grid-item>
</n-grid>
<!--导航卡片-->
<div class="mt-4">
<n-grid cols="1 s:2 m:3 l:8 xl:8 2xl:8" responsive="screen" :x-gap="16" :y-gap="8">
<n-grid-item v-for="(item, index) in iconList" :key="index" @click="item.eventObject || {}">
<NCard content-style="padding-top: 0;" size="small" :bordered="false">
<template #footer>
<n-skeleton v-if="loading" size="medium" />
<div class="cursor-pointer" v-else>
<p class="flex justify-center">
<span>
<n-icon :size="item.size" class="flex-1" :color="item.color">
<component :is="item.icon" v-on="item.eventObject || {}" />
</n-icon>
</span>
</p>
<p class="flex justify-center"
><span>{{ item.title }}</span></p
>
</div>
</template>
</NCard>
</n-grid-item>
</n-grid>
</div>
<!--访问量 | 流量趋势-->
<VisiTab />
<div>
<Company v-if="userStore.isCompanyDept" />
<Tenant v-else-if="userStore.isTenantDept" />
<Merchant v-else-if="userStore.isMerchantDept" />
<User v-else-if="userStore.isUserDept" />
<template v-else>
<div class="n-layout-page-header">
<n-card :bordered="false" title="默认首页">
部门类型未配置首页{{ userStore.info?.deptType }}
</n-card>
</div>
</template>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { getConsoleInfo } from '@/api/dashboard/console';
import VisiTab from './components/VisiTab.vue';
import { CountTo } from '@/components/CountTo/index';
import {
CaretUpOutlined,
CaretDownOutlined,
UsergroupAddOutlined,
BarChartOutlined,
ShoppingCartOutlined,
AccountBookOutlined,
CreditCardOutlined,
MailOutlined,
TagsOutlined,
SettingOutlined,
} from '@vicons/antd';
import Company from './console_company.vue';
import Merchant from './console_merchant.vue';
import User from './console_user.vue';
import Tenant from './console_tenant.vue';
import { useUserStore } from '@/store/modules/user';
const loading = ref(true);
const visits = ref<any>({});
const saleroom = ref<any>({});
const orderLarge = ref<any>({});
const volume = ref({});
const router = useRouter();
// 图标列表
const iconList = [
{
icon: UsergroupAddOutlined,
size: '32',
title: '用户',
color: '#69c0ff',
eventObject: {
click: () => router.push({ name: 'user' }),
},
},
{
icon: BarChartOutlined,
size: '32',
title: '分析',
color: '#69c0ff',
eventObject: {
click: () => {},
},
},
{
icon: ShoppingCartOutlined,
size: '32',
title: '商品',
color: '#ff9c6e',
eventObject: {
click: () => {},
},
},
{
icon: AccountBookOutlined,
size: '32',
title: '订单',
color: '#b37feb',
eventObject: {
click: () => {},
},
},
{
icon: CreditCardOutlined,
size: '32',
title: '票据',
color: '#ffd666',
eventObject: {
click: () => {},
},
},
{
icon: MailOutlined,
size: '32',
title: '消息',
color: '#5cdbd3',
eventObject: {
click: () => {},
},
},
{
icon: TagsOutlined,
size: '32',
title: '标签',
color: '#ff85c0',
eventObject: {
click: () => {},
},
},
{
icon: SettingOutlined,
size: '32',
title: '配置',
color: '#ffc069',
eventObject: {
click: () => {},
},
},
];
onMounted(async () => {
const data = await getConsoleInfo();
visits.value = data.visits;
saleroom.value = data.saleroom;
orderLarge.value = data.orderLarge;
volume.value = data.volume;
loading.value = false;
});
const userStore = useUserStore();
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,339 @@
<template>
<div class="console">
<!--数据卡片-->
<n-grid cols="1 s:2 m:3 l:4 xl:4 2xl:4" responsive="screen" :x-gap="12" :y-gap="8">
<n-grid-item>
<NCard
title="卡板量"
:segmented="{ content: true, footer: true }"
size="small"
:bordered="false"
>
<template #header-extra>
<n-tag type="success"></n-tag>
</template>
<div class="py-1 px-1 flex justify-between">
<n-skeleton v-if="loading" :width="100" size="medium" />
<CountTo v-else :startVal="1" :endVal="visits.dayVisits" class="text-3xl" />
</div>
<div class="py-1 px-1 flex justify-between">
<div class="text-sn">
<n-skeleton v-if="loading" :width="100" size="medium" />
<template v-else>
日同比
<CountTo :startVal="1" suffix="%" :endVal="visits.rise" />
<n-icon size="12" color="#00ff6f">
<CaretUpOutlined />
</n-icon>
</template>
</div>
<div class="text-sn">
<n-skeleton v-if="loading" :width="100" size="medium" />
<template v-else>
周同比
<CountTo :startVal="1" suffix="%" :endVal="visits.decline" />
<n-icon size="12" color="#ffde66">
<CaretDownOutlined />
</n-icon>
</template>
</div>
</div>
<template #footer>
<div class="flex justify-between">
<n-skeleton v-if="loading" text :repeat="2" />
<template v-else>
<div class="text-sn"> 总卡板量 </div>
<div class="text-sn">
<CountTo :startVal="1" :endVal="visits.amount" />
</div>
</template>
</div>
</template>
</NCard>
</n-grid-item>
<n-grid-item>
<NCard
title="激活卡板"
:segmented="{ content: true, footer: true }"
size="small"
:bordered="false"
>
<template #header-extra>
<n-tag type="info"></n-tag>
</template>
<div class="py-1 px-1 flex justify-between">
<n-skeleton v-if="loading" :width="100" size="medium" />
<CountTo
v-else
prefix="¥"
:startVal="1"
:endVal="saleroom.weekSaleroom"
class="text-3xl"
/>
</div>
<div class="py-2 px-2 flex justify-between">
<div class="text-sn flex-1">
<n-progress
type="line"
:percentage="saleroom.degree"
:indicator-placement="'inside'"
processing
/>
</div>
</div>
<template #footer>
<div class="flex justify-between">
<n-skeleton v-if="loading" :width="100" size="medium" />
<template v-else>
<div class="text-sn"> 总激活卡板 </div>
<div class="text-sn">
<CountTo :startVal="1" :endVal="saleroom.amount" />
<!-- prefix="¥"-->
</div>
</template>
</div>
</template>
</NCard>
</n-grid-item>
<n-grid-item>
<NCard
title="代理商"
:segmented="{ content: true, footer: true }"
size="small"
:bordered="false"
>
<template #header-extra>
<n-tag type="warning"></n-tag>
</template>
<div class="py-1 px-1 flex justify-between">
<n-skeleton v-if="loading" :width="100" size="medium" />
<CountTo v-else :startVal="1" :endVal="orderLarge.weekLarge" class="text-3xl" />
</div>
<div class="py-1 px-1 flex justify-between">
<div class="text-sn">
<n-skeleton v-if="loading" :width="100" size="medium" />
<template v-else>
日同比
<CountTo :startVal="1" suffix="%" :endVal="orderLarge.rise" />
<n-icon size="12" color="#00ff6f">
<CaretUpOutlined />
</n-icon>
</template>
</div>
<div class="text-sn">
<n-skeleton v-if="loading" :width="100" size="medium" />
<template v-else>
周同比
<CountTo :startVal="1" suffix="%" :endVal="orderLarge.rise" />
<n-icon size="12" color="#ffde66">
<CaretDownOutlined />
</n-icon>
</template>
</div>
</div>
<template #footer>
<div class="flex justify-between">
<n-skeleton v-if="loading" :width="100" size="medium" />
<template v-else>
<div class="text-sn"> 总代理商量 </div>
<div class="text-sn">
<CountTo :startVal="1" suffix="%" :endVal="orderLarge.amount" />
</div>
</template>
</div>
</template>
</NCard>
</n-grid-item>
<n-grid-item>
<NCard
title="提现佣金"
:segmented="{ content: true, footer: true }"
size="small"
:bordered="false"
>
<template #header-extra>
<n-tag type="error"></n-tag>
</template>
<div class="py-1 px-1 flex justify-between">
<n-skeleton v-if="loading" :width="100" size="medium" />
<CountTo v-else prefix="¥" :startVal="1" :endVal="volume.weekLarge" class="text-3xl" />
</div>
<div class="py-1 px-1 flex justify-between">
<div class="text-sn">
<n-skeleton v-if="loading" :width="100" size="medium" />
<template v-else>
月同比
<CountTo :startVal="1" suffix="%" :endVal="volume.rise" />
<n-icon size="12" color="#00ff6f">
<CaretUpOutlined />
</n-icon>
</template>
</div>
<div class="text-sn">
<n-skeleton v-if="loading" :width="100" size="medium" />
<template v-else>
月同比
<CountTo :startVal="1" suffix="%" :endVal="volume.decline" />
<n-icon size="12" color="#ffde66">
<CaretDownOutlined />
</n-icon>
</template>
</div>
</div>
<template #footer>
<div class="flex justify-between">
<n-skeleton v-if="loading" :width="100" size="medium" />
<template v-else>
<div class="text-sn"> 总提现额 </div>
<div class="text-sn">
<CountTo prefix="¥" :startVal="1" :endVal="volume.amount" />
</div>
</template>
</div>
</template>
</NCard>
</n-grid-item>
</n-grid>
<!--导航卡片-->
<div class="mt-4">
<n-grid cols="1 s:2 m:3 l:8 xl:8 2xl:8" responsive="screen" :x-gap="16" :y-gap="8">
<n-grid-item v-for="(item, index) in iconList" :key="index" @click="item.eventObject || {}">
<NCard content-style="padding-top: 0;" size="small" :bordered="false">
<template #footer>
<n-skeleton v-if="loading" size="medium" />
<div class="cursor-pointer" v-else>
<p class="flex justify-center">
<span>
<n-icon :size="item.size" class="flex-1" :color="item.color">
<component :is="item.icon" v-on="item.eventObject || {}" />
</n-icon>
</span>
</p>
<p class="flex justify-center"
><span>{{ item.title }}</span></p
>
</div>
</template>
</NCard>
</n-grid-item>
</n-grid>
</div>
<!--访问量 | 流量趋势-->
<VisiTab />
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { getConsoleInfo } from '@/api/dashboard/console';
import VisiTab from './components/VisiTab.vue';
import { CountTo } from '@/components/CountTo';
import {
CaretUpOutlined,
CaretDownOutlined,
UsergroupAddOutlined,
BarChartOutlined,
ShoppingCartOutlined,
AccountBookOutlined,
CreditCardOutlined,
MailOutlined,
TagsOutlined,
SettingOutlined,
} from '@vicons/antd';
const loading = ref(true);
const visits = ref<any>({});
const saleroom = ref<any>({});
const orderLarge = ref<any>({});
const volume = ref({});
const router = useRouter();
// 图标列表
const iconList = [
{
icon: UsergroupAddOutlined,
size: '32',
title: '用户',
color: '#69c0ff',
eventObject: {
click: () => router.push({ name: 'user' }),
},
},
{
icon: BarChartOutlined,
size: '32',
title: '分析',
color: '#69c0ff',
eventObject: {
click: () => {},
},
},
{
icon: ShoppingCartOutlined,
size: '32',
title: '商品',
color: '#ff9c6e',
eventObject: {
click: () => {},
},
},
{
icon: AccountBookOutlined,
size: '32',
title: '订单',
color: '#b37feb',
eventObject: {
click: () => {},
},
},
{
icon: CreditCardOutlined,
size: '32',
title: '票据',
color: '#ffd666',
eventObject: {
click: () => {},
},
},
{
icon: MailOutlined,
size: '32',
title: '消息',
color: '#5cdbd3',
eventObject: {
click: () => {},
},
},
{
icon: TagsOutlined,
size: '32',
title: '标签',
color: '#ff85c0',
eventObject: {
click: () => {},
},
},
{
icon: SettingOutlined,
size: '32',
title: '配置',
color: '#ffc069',
eventObject: {
click: () => {},
},
},
];
onMounted(async () => {
const data = await getConsoleInfo();
visits.value = data.visits;
saleroom.value = data.saleroom;
orderLarge.value = data.orderLarge;
volume.value = data.volume;
loading.value = false;
});
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,11 @@
<script setup lang="ts"></script>
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="商户首页"> 这是商户的首页如果需要你可以定制TA </n-card>
</div>
</div>
</template>
<style scoped lang="less"></style>

View File

@@ -0,0 +1,11 @@
<script setup lang="ts"></script>
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="租户首页"> 这是租户的首页如果需要你可以定制TA </n-card>
</div>
</div>
</template>
<style scoped lang="less"></style>

View File

@@ -0,0 +1,13 @@
<script setup lang="ts"></script>
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="用户首页">
这是普通用户的首页如果需要你可以定制TA
</n-card>
</div>
</div>
</template>
<style scoped lang="less"></style>

View File

@@ -41,6 +41,7 @@
<n-modal
v-model:show="showModal"
:mask-closable="false"
:show-icon="false"
preset="dialog"
title="创建新插件"
@@ -127,12 +128,12 @@
</template>
<script lang="ts" setup>
import { h, reactive, ref } from 'vue';
import { computed, h, reactive, ref } from 'vue';
import { NIcon, useMessage, useDialog, useNotification } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, useForm } from '@/components/Form/index';
import { List, Build, UnInstall, Install, Upgrade } from '@/api/develop/addons';
import { PlusOutlined, QuestionCircleOutlined } from '@vicons/antd';
import { PlusOutlined } from '@vicons/antd';
import { newState, schemas, columns, options } from './model';
import { adaModalWidth } from '@/utils/hotgo';
@@ -144,15 +145,17 @@
const formRef: any = ref(null);
const actionRef = ref();
const formParams = ref<any>();
const dialogWidth = ref('50%');
const checkedIds = ref([]);
const searchFormRef = ref<any>();
const dialogWidth = computed(() => {
return adaModalWidth();
});
const actionColumn = reactive({
width: 220,
title: '操作',
key: 'action',
// fixed: 'right',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
@@ -196,7 +199,6 @@
}
const loadDataTable = async (res) => {
adaModalWidth(dialogWidth);
return await List({ ...res, ...searchFormRef.value?.formModel });
};

View File

@@ -60,6 +60,54 @@
</n-form-item>
</n-col>
<!-- 树表-->
<template v-if="formValue.genType == 11">
<n-col :span="6" style="min-width: 200px">
<n-form-item path="title">
<template #label>
<div class="flex flex-row items-end"
>树名称字段
<n-tooltip trigger="hover">
<template #trigger>
<n-button strong text>
<template #icon>
<n-icon size="15" color="#2d8cf0">
<QuestionCircleOutlined />
</n-icon>
</template>
</n-button>
</template>
树节点的显示名称字段名 `title`
</n-tooltip>
</div>
</template>
<n-select
filterable
tag
:loading="columnsLoading"
placeholder="请选择"
:options="columnsOption"
v-model:value="formValue.options.tree.titleColumn"
/>
</n-form-item>
</n-col>
<n-col :span="6" style="min-width: 200px">
<n-form-item label="树表格样式" path="styleType">
<n-radio-group v-model:value="formValue.options.tree.styleType" name="styleType">
<n-radio
v-for="status in selectList.treeStyleType"
:value="status.value"
:label="status.label"
>{{ status.label }}</n-radio
>
</n-radio-group>
</n-form-item>
</n-col>
</template>
<!-- 树表-->
<n-col :span="18">
<n-form-item
label="表格头部按钮组"
@@ -97,15 +145,6 @@
<n-checkbox value="del" label="删除" />
<n-checkbox value="view" label="详情页" />
<n-checkbox value="check" label="开启勾选列" />
<n-checkbox value="switch" label="操作开关" />
<n-popover trigger="hover">
<template #trigger>
<n-icon size="15" class="tips-help-icon" color="#2d8cf0">
<QuestionCircleOutlined />
</n-icon>
</template>
<span>表单组件中存在`开关`类型才会生效</span>
</n-popover>
<n-checkbox value="notFilterAuth" label="不过滤权限" />
<n-popover trigger="hover">
<template #trigger>
@@ -122,7 +161,7 @@
<n-col :span="24">
<n-form-item
label="自动化操作"
label="高级设置"
path="autoOps"
v-show="formValue.genType >= 10 && formValue.genType < 20"
>
@@ -136,7 +175,7 @@
<QuestionCircleOutlined />
</n-icon>
</template>
<span>如果你选择的表已经生成过dao相关代码可以忽略</span>
<span>如果你选择的表已经生成过dao相关代码取消勾选可减少生成时间</span>
</n-popover>
<n-checkbox value="runService" label="生成后运行 [gf gen service]" />
<n-popover trigger="hover">
@@ -147,6 +186,19 @@
</template>
<span>如果是插件模块勾选后也会自动在对应插件下运行service相关代码生成</span>
</n-popover>
<n-checkbox
value="genFuncDict"
label="生成字典选项"
@click="handleCheckboxGenFuncDict"
/>
<n-popover trigger="hover">
<template #trigger>
<n-icon size="15" class="tips-help-icon" color="#2d8cf0">
<QuestionCircleOutlined />
</n-icon>
</template>
<span>将表数据生成为数据选项并注册到内置的方法字典</span>
</n-popover>
<n-checkbox value="forcedCover" label="强制覆盖" />
<n-popover trigger="hover">
<template #trigger>
@@ -166,7 +218,13 @@
style="min-width: 200px"
v-show="formValue.options?.autoOps?.includes('genMenuPermissions')"
>
<n-form-item label="上级菜单" path="pid">
<n-form-item path="pid">
<template #label>
<span>上级菜单</span>
<n-button class="ml-2" text type="primary" strong @click="handleAddMenu"
>菜单管理</n-button
>
</template>
<n-tree-select
:options="optionMenuTree"
:value="formValue.options.menu.pid"
@@ -224,16 +282,16 @@
<template #header-extra>
<n-space>
<n-button
type="warning"
type="primary"
@click="addJoin"
:disabled="formValue.options?.join?.length >= 3"
:disabled="formValue.options?.join?.length >= 20"
>新增关联表</n-button
>
</n-space>
</template>
<n-form ref="formRef" :model="formValue">
<n-alert :show-icon="false">关联表数量建议在三个以下</n-alert>
<n-alert type="warning" :show-icon="false" v-if="formValue.options?.join?.length > 3">关联表数量建议在三个以下</n-alert>
<div class="mt-4"></div>
<n-row :gutter="6" v-for="(join, index) in formValue.options.join" :key="index">
<n-col :span="6" style="min-width: 200px">
@@ -315,12 +373,19 @@
</n-form>
</n-card>
</n-spin>
<MenuModal ref="menuModalRef" @reloadTable="loadMenuTreeOption" />
<SetFuncDict
ref="setFuncDictRef"
@update="handleUpdateFuncDict"
:columnsOption="columnsOption"
/>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, computed, watch } from 'vue';
import { FormInst } from 'naive-ui';
import { FormInst, useDialog, useMessage } from 'naive-ui';
import { newState, selectListObj } from './model';
import { TableSelect, ColumnSelect } from '@/api/develop/code';
import { getRandomString } from '@/utils/charset';
@@ -329,7 +394,11 @@
import { getMenuList } from '@/api/system/menu';
import { cloneDeep } from 'lodash-es';
import { isLetterBegin } from '@/utils/is';
import MenuModal from '@/views/permission/menu/menuModal.vue';
import SetFuncDict from './SetFuncDict.vue';
const message = useMessage();
const dialog = useDialog();
const timer = ref();
const formRef = ref<FormInst | null>(null);
const bodyShow = ref(true);
@@ -340,6 +409,8 @@
const columnsOption = ref<any>([]); // 主表字段选项
const linkTablesOption = ref<any>([]); // 关联表选项
const linkColumnsOption = ref<any>([]); // 关联表字段选项
const menuModalRef = ref();
const setFuncDictRef = ref();
const optionMenuTree = ref([
{
@@ -515,6 +586,20 @@
function handleUpdateMenuPid(value: string | number | Array<string | number> | null) {
formValue.value.options.menu.pid = value;
}
function handleAddMenu() {
menuModalRef.value.openModal();
}
function handleCheckboxGenFuncDict() {
if (formValue.value.options.autoOps.includes('genFuncDict')) {
setFuncDictRef.value.openModal(formValue.value.options.funcDict);
}
}
function handleUpdateFuncDict(value) {
formValue.value.options.funcDict = value;
}
</script>
<style lang="less" scoped>

View File

@@ -1,58 +1,85 @@
<template>
<n-spin :show="show" description="加载中...">
<n-card :bordered="false" class="proCard">
<n-card :bordered="false" class="proCard">
<n-spin :show="show" description="加载中...">
<BasicTable
:single-line="false"
size="small"
:striped="true"
:resizable="true"
striped
resizable
canResize
virtual-scroll
:single-line="false"
:showTopRight="false"
:pagination="false"
:columns="columns"
:dataSource="dataSource"
:openChecked="false"
:showTopRight="false"
:row-key="(row) => row.id"
ref="actionRef"
:canResize="true"
:pagination="false"
:scroll-x="3000"
:resizeHeightOffset="-20000"
:scroll-x="columnCollapse ? 1400 : 2400"
:scroll-y="720"
:scrollbar-props="{ trigger: 'none' }"
>
<template #tableTitle>
<n-tooltip placement="top-start" trigger="hover">
<template #trigger>
<n-button type="primary" @click="reloadFields(true)" class="min-left-space">
<template #icon>
<n-icon>
<Reload />
</n-icon>
</template>
重置字段
</n-button>
</template>
主要用于重置字段设置或数据库表字段发生变化时重新载入
</n-tooltip>
<n-space>
<n-popconfirm @positive-click="reloadColumns">
<template #trigger>
<n-button type="primary" class="min-left-space">
<template #icon>
<n-icon>
<Reload />
</n-icon>
</template>
重置字段
</n-button>
</template>
重置后将从数据库重新加载表,不保留当前字段配置,确定要重置吗?
</n-popconfirm>
<n-popconfirm @positive-click="syncColumns">
<template #trigger>
<n-button type="primary" class="min-left-space">
<template #icon>
<n-icon>
<Sync />
</n-icon>
</template>
同步字段
</n-button>
</template>
同步是从数据库重新加载表,保留当前有效的字段配置,确定要同步吗?
</n-popconfirm>
<n-button type="default" class="min-left-space" @click="handleMove">
<template #icon>
<n-icon>
<MoveOutline />
</n-icon>
</template>
移动字段
</n-button>
</n-space>
</template>
</BasicTable>
</n-card>
</n-spin>
</n-spin>
</n-card>
<Move ref="moveRef" v-model:columns="dataSource" />
</template>
<script lang="ts" setup>
import { computed, h, onMounted, ref } from 'vue';
import { BasicTable } from '@/components/Table';
import { genInfoObj, selectListObj } from '@/views/develop/code/components/model';
import {
formatColumns,
formGridColsOptions,
formGridSpanOptions,
genInfoObj,
selectListObj,
} from '@/views/develop/code/components/model';
import { ColumnList } from '@/api/develop/code';
import { NButton, NCheckbox, NInput, NSelect, NTooltip, NTreeSelect,NCascader } from 'naive-ui';
import { HelpCircleOutline, Reload } from '@vicons/ionicons5';
import { renderIcon } from '@/utils';
import { NInputNumber, NSpace, NButton, NCheckbox, NInput, NSelect, NCascader } from 'naive-ui';
import { HelpCircleOutline, Reload, Sync, MoveOutline } from '@vicons/ionicons5';
import { cloneDeep } from 'lodash-es';
const renderTooltip = (trigger, content) => {
return h(NTooltip, null, {
trigger: () => trigger,
default: () => content,
});
};
import { renderIcon, renderTooltip } from '@/utils';
import Move from './Move.vue';
const emit = defineEmits(['update:value']);
@@ -75,93 +102,22 @@
},
});
const actionRef = ref();
const columns = ref<any>([]);
const show = ref(false);
const dataSource = ref(formValue.value.masterColumns);
async function reloadFields(loading = false) {
dataSource.value = [];
if (loading) {
show.value = true;
}
formValue.value.masterColumns = await ColumnList({
name: formValue.value.dbName,
table: formValue.value.tableName,
});
dataSource.value = formValue.value.masterColumns;
if (loading) {
show.value = false;
}
}
onMounted(async () => {
show.value = true;
if (formValue.value.masterColumns.length === 0) {
await reloadFields();
}
columns.value = [
{
title: '位置',
key: 'id',
width: 50,
},
{
title(_column) {
return renderTooltip(
h(
NButton,
{
strong: true,
size: 'small',
text: true,
iconPlacement: 'right',
},
{ default: () => '字段', icon: renderIcon(HelpCircleOutline) }
),
'Go类型和属性定义取决于你在/hack/config.yaml中的配置参数'
);
},
key: 'field',
align: 'center',
width: 800,
children: [
const dataSource = ref([]);
const moveRef = ref();
const columnCollapse = ref(true);
const columnsCollapseData = computed(() => {
return columnCollapse.value
? [
{
title: '字段列名',
key: 'name',
width: 150,
},
{
title: '物理类型',
key: 'sqlType',
width: 150,
},
{
title: 'Go属性',
key: 'goName',
width: 130,
},
{
title: 'Go类型',
key: 'goType',
width: 100,
},
{
title: 'Ts属性',
key: 'tsName',
width: 130,
},
{
title: 'Ts类型',
key: 'tsType',
width: 100,
},
{
title: '字段描述',
key: 'dc',
width: 150,
width: 100,
render(row) {
return h(NInput, {
value: row.dc,
@@ -171,7 +127,98 @@
});
},
},
],
]
: [
{
title: '字段列名',
key: 'name',
width: 100,
},
{
title: '物理类型',
key: 'sqlType',
width: 80,
},
{
title: 'Go属性',
key: 'goName',
width: 100,
},
{
title: 'Go类型',
key: 'goType',
width: 80,
},
{
title: 'Ts属性',
key: 'tsName',
width: 100,
},
{
title: 'Ts类型',
key: 'tsType',
width: 80,
},
{
title: '字段描述',
key: 'dc',
width: 100,
render(row) {
return h(NInput, {
value: row.dc,
onUpdateValue: function (e) {
row.dc = e;
},
});
},
},
];
});
const columns = computed(() => {
return [
{
title: '',
key: 'id',
width: 30,
render(row, index) {
return index + 1;
},
},
{
title(_column) {
return h('div', null, [
renderTooltip(
h(
NButton,
{
strong: true,
size: 'small',
text: true,
iconPlacement: 'right',
},
{ default: () => '字段', icon: renderIcon(HelpCircleOutline) }
),
'Go类型和属性定义取决于你在/hack/config.yaml中的配置参数'
),
h(
NButton,
{
strong: true,
size: 'small',
text: true,
type: 'primary',
style: { 'margin-left': '20px' },
onClick: () => (columnCollapse.value = !columnCollapse.value),
},
{ default: () => (columnCollapse.value ? '展开 >>' : '折叠 <<') }
),
]);
},
key: 'field',
align: 'center',
width: 800,
children: columnsCollapseData.value,
},
{
width: 800,
@@ -197,21 +244,26 @@
align: 'center',
title: '编辑',
key: 'isEdit',
width: 50,
width: 30,
render(row) {
return h(NCheckbox, {
const disabled = isEditDisabled(row);
const checkbox = h(NCheckbox, {
defaultChecked: row.isEdit,
disabled: row.name === 'id',
disabled: disabled,
onUpdateChecked: function (e) {
row.isEdit = e;
},
});
if (!disabled) {
return checkbox;
}
return renderTooltip(checkbox, '该字段属性由系统维护,无需单独配置!');
},
},
{
title: '必填',
key: 'required',
width: 50,
width: 30,
align: 'center',
render(row) {
return h(NCheckbox, {
@@ -226,7 +278,7 @@
{
title: '唯一',
key: 'unique',
width: 50,
width: 30,
align: 'center',
render(row) {
return h(NCheckbox, {
@@ -241,17 +293,12 @@
{
title: '表单组件',
key: 'formMode',
width: 200,
width: 100,
render(row) {
return h(NSelect, {
consistentMenuWidth: false,
value: row.formMode,
options: getFormModeOptions(row.tsType),
// render: function (row) {
// return props.selectList?.formMode ?? [];
// },
// onFocus: function (e) {
// console.log('表单组件 onFocus row:', e);
// },
onUpdateValue: function (e) {
row.formMode = e;
},
@@ -259,11 +306,35 @@
},
},
{
title: '表单验证',
title: '绑定字典',
key: 'dictType',
width: 100,
render(row) {
if (row.dictType == 0) {
row.dictType = null;
}
return h(NCascader, {
placeholder: ' ',
filterable: true,
clearable: true,
showPath: false,
checkStrategy: 'child',
disabled: row.name === 'id',
value: row.dictType,
options: props.selectList?.dictMode ?? [],
onUpdateValue: function (e) {
row.dictType = e;
},
});
},
},
{
title: '验证规则',
key: 'formRole',
width: 200,
width: 100,
render(row) {
return h(NSelect, {
consistentMenuWidth: false,
value: row.formRole,
disabled: row.name === 'id',
options: props.selectList?.formRole ?? [],
@@ -274,22 +345,43 @@
},
},
{
title: '字典类型',
key: 'dictType',
width: 300,
title(_column) {
return h(NSpace, { inline: true }, [
renderTooltip(
h(
NButton,
{
strong: true,
size: 'small',
text: true,
iconPlacement: 'right',
},
{ default: () => '栅格', icon: renderIcon(HelpCircleOutline) }
),
'表单每行摆放组件的个数。响应式栅格小屏幕自动转为每行摆放一个组件。参考文档https://www.naiveui.com/zh-CN/os-theme/components/grid#responsive-item.vue'
),
h(NSelect, {
style: { width: '100px' },
size: 'small',
consistentMenuWidth: false,
value: formValue.value.options.presetStep.formGridCols,
options: formGridColsOptions,
onUpdateValue: function (e) {
formValue.value.options.presetStep.formGridCols = e;
},
}),
]);
},
key: 'formGridSpan',
width: 120,
render(row) {
if (row.dictType == 0){
row.dictType = null;
}
return h(NCascader, {
placeholder: '请选择字典类型',
filterable: true,
clearable: true,
return h(NSelect, {
consistentMenuWidth: false,
disabled: row.name === 'id',
value: row.dictType,
options: props.selectList?.dictMode ?? [],
value: row.formGridSpan,
options: getFormGridSpanOptions(formValue.value.options.presetStep.formGridCols),
onUpdateValue: function (e) {
row.dictType = e;
row.formGridSpan = e;
},
});
},
@@ -305,7 +397,7 @@
{
title: '列表',
key: 'isList',
width: 50,
width: 30,
align: 'center',
render(row) {
return h(NCheckbox, {
@@ -319,7 +411,7 @@
{
title: '导出',
key: 'isExport',
width: 50,
width: 30,
align: 'center',
render(row) {
return h(NCheckbox, {
@@ -333,7 +425,7 @@
{
title: '查询',
key: 'isQuery',
width: 50,
width: 30,
align: 'center',
render(row) {
return h(NCheckbox, {
@@ -347,9 +439,10 @@
{
title: '查询条件',
key: 'queryWhere',
width: 300,
width: 90,
render(row) {
return h(NSelect, {
consistentMenuWidth: false,
value: row.queryWhere,
disabled: row.name === 'id',
options: props.selectList?.whereMode ?? [],
@@ -359,13 +452,108 @@
});
},
},
{
title: '排列方式',
key: 'align',
width: 80,
render(row) {
return h(NSelect, {
consistentMenuWidth: false,
value: row.align,
options: props.selectList?.tableAlign ?? [],
onUpdateValue: function (e) {
row.align = e;
},
});
},
},
{
title(_column) {
return renderTooltip(
h(
NButton,
{
strong: true,
size: 'small',
text: true,
iconPlacement: 'right',
},
{ default: () => '列宽', icon: renderIcon(HelpCircleOutline) }
),
'选填。设定固定值时表格生成自动计算scroll-x未设定默认每列按100计算'
);
},
key: 'width',
width: 50,
render(row) {
return h(NInputNumber, {
value: row.width,
placeholder: ' ',
min: -1,
max: 2000,
showButton: false,
onUpdateValue: function (e) {
row.width = e;
},
});
},
},
],
},
];
show.value = false;
});
// 同步字段
function syncColumns() {
show.value = true;
dataSource.value = [];
const params = {
name: formValue.value.dbName,
table: formValue.value.tableName,
};
ColumnList(params)
.then((res) => {
const columns = formatColumns(res);
for (let i = 0; i < columns.length; i++) {
// 相同字段名称和类型,保留原字段属性
const index = formValue.value.masterColumns.findIndex(
(item) => item.name == columns[i].name && item.dataType == columns[i].dataType
);
if (index !== -1) {
columns[i] = formValue.value.masterColumns[index];
}
}
formValue.value.masterColumns = columns;
dataSource.value = formValue.value.masterColumns;
})
.finally(() => {
show.value = false;
});
}
// 重载字段属性
function reloadColumns() {
show.value = true;
dataSource.value = [];
const params = {
name: formValue.value.dbName,
table: formValue.value.tableName,
};
ColumnList(params)
.then((res) => {
formValue.value.masterColumns = formatColumns(res);
dataSource.value = formValue.value.masterColumns;
})
.finally(() => {
show.value = false;
});
}
function getFormModeOptions(type: string) {
const options = cloneDeep(props.selectList?.formMode ?? []);
if (options.length === 0) {
@@ -374,7 +562,16 @@
switch (type) {
case 'number':
for (let i = 0; i < options.length; i++) {
const allows = ['InputNumber', 'Radio', 'Select', 'Switch', 'Rate'];
const allows = [
'Input',
'InputNumber',
'Radio',
'Select',
'Switch',
'Rate',
'TreeSelect',
'Cascader',
];
if (!allows.includes(options[i].value)) {
options[i].disabled = true;
}
@@ -382,8 +579,73 @@
break;
default:
}
options.sort((a, b) => (a.disabled === b.disabled ? 0 : a.disabled ? 1 : -1));
return options;
}
function getFormGridSpanOptions(cols: number) {
if (cols < 1) {
cols = 1;
}
if (cols > 4) {
cols = 4;
}
for (let i = 0; i < formValue.value.masterColumns.length; i++) {
if (!formValue.value.masterColumns[i].formGridSpan) {
formValue.value.masterColumns[i].formGridSpan = 1;
}
if (formValue.value.masterColumns[i].formGridSpan > cols) {
formValue.value.masterColumns[i].formGridSpan = cols;
}
}
return formGridSpanOptions.slice(0, Math.min(cols, formGridSpanOptions.length));
}
// 禁止编辑的字段,由系统维护
function isEditDisabled(row) {
const disabledNames = [
'id',
'created_by',
'updated_by',
'deleted_by',
'created_at',
'updated_at',
'deleted_at',
];
if (disabledNames.includes(row.name)) {
return true;
}
if (formValue.value.genType == 11) {
const disabledTreeNames = ['pid', 'level', 'tree'];
if (disabledTreeNames.includes(row.name)) {
return true;
}
}
return false;
}
function handleMove() {
moveRef.value.openModal();
}
onMounted(() => {
if (formValue.value.masterColumns.length === 0) {
reloadColumns();
} else {
show.value = true;
setTimeout(function () {
dataSource.value = formValue.value.masterColumns;
show.value = false;
}, 100);
}
});
</script>
<style lang="less" scoped></style>
<style lang="less" scoped>
.tree-tips {
margin-left: 12px;
color: #18a058;
font-weight: 600;
}
</style>

View File

@@ -2,43 +2,85 @@
<n-spin :show="show" description="加载中...">
<n-card :bordered="false" class="proCard">
<BasicTable
:single-line="false"
size="small"
:striped="true"
:resizable="true"
striped
resizable
canResize
:single-line="false"
:showTopRight="false"
:pagination="false"
:columns="columns"
:dataSource="dataSource"
:openChecked="false"
:showTopRight="false"
:row-key="(row) => row.id"
ref="actionRef"
:canResize="true"
:resizeHeightOffset="-20000"
:pagination="false"
:scroll-x="1090"
:scroll-x="columnCollapse ? 880 : 1880"
:scroll-y="720"
:scrollbar-props="{ trigger: 'none' }"
/>
>
<template #tableTitle>
<n-space>
<n-popconfirm @positive-click="reloadColumns(getIndex())">
<template #trigger>
<n-button type="primary" class="min-left-space">
<template #icon>
<n-icon>
<Reload />
</n-icon>
</template>
重置字段
</n-button>
</template>
重置后将从数据库重新加载该表的默认配置,确定要重置吗?
</n-popconfirm>
<n-popconfirm @positive-click="syncColumns(getIndex())">
<template #trigger>
<n-button type="primary" class="min-left-space">
<template #icon>
<n-icon>
<Sync />
</n-icon>
</template>
同步字段
</n-button>
</template>
同步是从数据库重新加载表,保留当前有效的字段配置,确定要同步吗?
</n-popconfirm>
<n-button type="default" class="min-left-space" @click="handleMove">
<template #icon>
<n-icon>
<MoveOutline />
</n-icon>
</template>
移动字段
</n-button>
</n-space>
</template>
</BasicTable>
</n-card>
<Move ref="moveRef" v-model:columns="dataSource" />
</n-spin>
</template>
<script lang="ts" setup>
import { Component, computed, h, onMounted, ref } from 'vue';
import { computed, h, onMounted, ref } from 'vue';
import { BasicTable } from '@/components/Table';
import { genInfoObj, selectListObj } from '@/views/develop/code/components/model';
import { formatColumns, genInfoObj, selectListObj } from '@/views/develop/code/components/model';
import { ColumnList } from '@/api/develop/code';
import { NButton, NCheckbox, NIcon, NInput, NSelect, NTooltip } from 'naive-ui';
import { HelpCircleOutline } from '@vicons/ionicons5';
const renderTooltip = (trigger, content) => {
return h(NTooltip, null, {
trigger: () => trigger,
default: () => content,
});
};
function renderIcon(icon: Component) {
return () => h(NIcon, null, { default: () => h(icon) });
}
import {
NButton,
NCheckbox,
NIcon,
NInput,
NInputNumber,
NSelect,
NSpace,
NTooltip,
} from 'naive-ui';
import { HelpCircleOutline, MoveOutline, Reload, Sync, WarningOutline } from '@vicons/ionicons5';
import { renderIcon, renderTooltip } from '@/utils';
import Move from './Move.vue';
const emit = defineEmits(['update:value']);
@@ -54,8 +96,6 @@
uuid: '',
});
const columns = ref<any>([]);
const formValue = computed({
get() {
return props.value;
@@ -65,6 +105,243 @@
},
});
const show = ref(false);
const dataSource = ref([]);
const moveRef = ref();
const columnCollapse = ref(true);
const columnsCollapseData = computed(() => {
return columnCollapse.value
? [
{
title: '字段列名',
key: 'name',
width: 150,
},
{
title: '字段描述',
key: 'dc',
width: 150,
render(row) {
return h(NInput, {
value: row.dc,
onUpdateValue: function (e) {
row.dc = e;
// await saveProductCustom(row.id, 'frontShow', e);
},
});
},
},
]
: [
{
title: '字段列名',
key: 'name',
width: 100,
},
{
title: '物理类型',
key: 'sqlType',
width: 80,
},
{
title: 'Go属性',
key: 'goName',
width: 150,
},
{
title: 'Go类型',
key: 'goType',
width: 100,
},
{
title: 'Ts属性',
key: 'tsName',
width: 150,
},
{
title: 'Ts类型',
key: 'tsType',
width: 100,
},
{
title: '字段描述',
key: 'dc',
width: 100,
render(row) {
return h(NInput, {
value: row.dc,
onUpdateValue: function (e) {
row.dc = e;
},
});
},
},
];
});
const columns = computed(() => {
return [
{
title: '',
key: 'id',
width: 50,
render(row, index) {
return index + 1;
},
},
{
title(_column) {
return h('div', null, [
renderTooltip(
h(
NButton,
{
strong: true,
size: 'small',
text: true,
iconPlacement: 'right',
},
{ default: () => '字段', icon: renderIcon(HelpCircleOutline) }
),
'Go类型和属性定义取决于你在/hack/config.yaml中的配置参数'
),
h(
NButton,
{
strong: true,
size: 'small',
text: true,
type: 'primary',
style: { 'margin-left': '20px' },
onClick: () => (columnCollapse.value = !columnCollapse.value),
},
{ default: () => (columnCollapse.value ? '展开 >>' : '折叠 <<') }
),
]);
},
key: 'field',
align: 'center',
width: 800,
children: columnsCollapseData.value,
},
{
width: 800,
title: '列表',
key: 'list',
align: 'center',
children: [
{
title: '列表',
key: 'isList',
width: 50,
align: 'center',
render(row) {
return h(NCheckbox, {
defaultChecked: row.isList,
onUpdateChecked: function (e) {
row.isList = e;
},
});
},
},
{
title: '导出',
key: 'isExport',
width: 50,
align: 'center',
render(row) {
return h(NCheckbox, {
defaultChecked: row.isExport,
onUpdateChecked: function (e) {
row.isExport = e;
},
});
},
},
{
title: '查询',
key: 'isQuery',
width: 50,
align: 'center',
render(row) {
return h(NCheckbox, {
defaultChecked: row.isQuery,
onUpdateChecked: function (e) {
row.isQuery = e;
},
});
},
},
{
title: '查询条件',
key: 'queryWhere',
width: 100,
render(row) {
return h(NSelect, {
consistentMenuWidth: false,
value: row.queryWhere,
disabled: row.name === 'id',
options: props.selectList?.whereMode ?? [],
onUpdateValue: function (e) {
row.queryWhere = e;
},
});
},
},
{
title: '排列方式',
key: 'align',
width: 100,
render(row) {
return h(NSelect, {
consistentMenuWidth: false,
value: row.align,
options: props.selectList?.tableAlign ?? [],
onUpdateValue: function (e) {
row.align = e;
},
});
},
},
{
title(_column) {
return renderTooltip(
h(
NButton,
{
strong: true,
size: 'small',
text: true,
iconPlacement: 'right',
},
{ default: () => '列宽', icon: renderIcon(HelpCircleOutline) }
),
'选填。设定固定值时表格生成自动计算scroll-x未设定默认每列按100计算'
);
},
key: 'width',
width: 50,
render(row) {
return h(NInputNumber, {
value: row.width,
placeholder: ' ',
min: 0,
max: 2000,
showButton: false,
onUpdateValue: function (e) {
row.width = e;
},
});
},
},
],
},
];
});
function handleMove() {
moveRef.value.openModal();
}
function getIndex() {
if (formValue.value.options.join.length === 0) {
return -1;
@@ -77,166 +354,77 @@
return -1;
}
const show = ref(false);
const dataSource = ref([]);
onMounted(async () => {
// 同步字段属性
function syncColumns(index: number) {
show.value = true;
setTimeout(async () => {
dataSource.value = [];
const join = formValue.value.options.join[index];
const params = {
name: formValue.value.dbName,
table: join.linkTable,
isLink: 1,
alias: join.alias,
};
ColumnList(params)
.then((res) => {
const columns = formatColumns(res);
for (let i = 0; i < columns.length; i++) {
// 相同字段名称和类型,保留原字段属性
const index2 = join.columns.findIndex(
(item) => item.name == columns[i].name && item.dataType == columns[i].dataType
);
if (index2 !== -1) {
columns[i] = join.columns[index2];
}
}
join.columns = columns;
dataSource.value = join.columns;
})
.finally(() => {
show.value = false;
});
}
// 重载字段属性
function reloadColumns(index: number) {
show.value = true;
dataSource.value = [];
const join = formValue.value.options.join[index];
const params = {
name: formValue.value.dbName,
table: join.linkTable,
isLink: 1,
alias: join.alias,
};
ColumnList(params)
.then((res) => {
join.columns = formatColumns(res);
dataSource.value = join.columns;
})
.finally(() => {
show.value = false;
});
}
onMounted(() => {
show.value = true;
setTimeout(() => {
const index = getIndex();
if (formValue.value.options.join[index].columns.length === 0) {
formValue.value.options.join[index].columns = await ColumnList({
name: formValue.value.dbName,
table: formValue.value.options.join[index].linkTable,
isLink: 1,
alias: formValue.value.options.join[index].alias,
});
// 已存在直接加载
if (formValue.value.options.join[index].columns.length > 0) {
dataSource.value = formValue.value.options.join[index].columns;
show.value = false;
return;
}
dataSource.value = formValue.value.options.join[index].columns;
columns.value = [
{
title: '位置',
key: 'id',
width: 50,
},
{
title(_column) {
return renderTooltip(
h(
NButton,
{
strong: true,
size: 'small',
text: true,
iconPlacement: 'right',
},
{ default: () => '字段', icon: renderIcon(HelpCircleOutline) }
),
'Go类型和属性定义取决于你在/hack/config.yaml中的配置参数'
);
},
key: 'field',
align: 'center',
width: 800,
children: [
{
title: '字段列名',
key: 'name',
width: 150,
},
{
title: '物理类型',
key: 'sqlType',
width: 150,
},
{
title: 'Go属性',
key: 'goName',
width: 260,
},
{
title: 'Go类型',
key: 'goType',
width: 100,
},
{
title: 'Ts属性',
key: 'tsName',
width: 260,
},
{
title: 'Ts类型',
key: 'tsType',
width: 100,
},
{
title: '字段描述',
key: 'dc',
width: 150,
render(row) {
return h(NInput, {
value: row.dc,
onUpdateValue: function (e) {
row.dc = e;
// await saveProductCustom(row.id, 'frontShow', e);
},
});
},
},
],
},
{
width: 800,
title: '列表',
key: 'list',
align: 'center',
children: [
{
title: '列表',
key: 'isList',
width: 50,
align: 'center',
render(row) {
return h(NCheckbox, {
defaultChecked: row.isList,
onUpdateChecked: function (e) {
row.isList = e;
},
});
},
},
{
title: '导出',
key: 'isExport',
width: 50,
align: 'center',
render(row) {
return h(NCheckbox, {
defaultChecked: row.isExport,
onUpdateChecked: function (e) {
row.isExport = e;
},
});
},
},
{
title: '查询',
key: 'isQuery',
width: 50,
align: 'center',
render(row) {
return h(NCheckbox, {
defaultChecked: row.isQuery,
onUpdateChecked: function (e) {
row.isQuery = e;
},
});
},
},
{
title: '查询条件',
key: 'queryWhere',
width: 300,
render(row) {
return h(NSelect, {
value: row.queryWhere,
disabled: row.name === 'id',
options: props.selectList?.whereMode ?? [],
onUpdateValue: function (e) {
row.queryWhere = e;
},
});
},
},
],
},
];
show.value = false;
}, 50);
reloadColumns(index);
}, 100);
});
const actionRef = ref();
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,82 @@
<template>
<div>
<n-modal
v-model:show="showModal"
:mask-closable="false"
:show-icon="false"
preset="dialog"
transform-origin="center"
title="移动字段"
:style="{
width: dialogWidth,
}"
>
<n-scrollbar style="max-height: 87vh" class="pr-5">
<n-card
:bordered="false"
:content-style="{ padding: '0px' }"
:header-style="{ padding: 'px' }"
:segmented="true"
>
请通过拖拽来移动字段的位置
<div class="mt-8"></div>
<Draggable
class="draggable-ul"
animation="300"
:list="columns"
group="people"
itemKey="name"
>
<template #item="{ element }">
<div class="cursor-move draggable-li">
<n-tag type="default" size="small" style="font-weight: 800">{{
element.name
}}</n-tag
><span class="ml-2">{{ element.dc }}</span>
</div>
</template>
</Draggable>
</n-card>
</n-scrollbar>
</n-modal>
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import Draggable from 'vuedraggable';
import { adaModalWidth } from '@/utils/hotgo';
const showModal = ref(false);
const columns = defineModel<[]>('columns');
const dialogWidth = computed(() => {
return adaModalWidth(360);
});
function openModal() {
showModal.value = true;
}
defineExpose({
openModal,
});
</script>
<style lang="less" scoped>
.draggable-ul {
width: 100%;
overflow: hidden;
margin-top: -8px;
.draggable-li {
width: 100%;
padding: 8px 4px;
color: #333;
border-bottom: 1px solid #efeff5;
}
.draggable-li:hover {
background-color: rgba(229, 231, 235, var(--tw-border-opacity));
}
}
</style>

View File

@@ -1,16 +1,28 @@
<template>
<div>
<textarea id="copy-code" :value="content"></textarea>
<n-tabs type="line" animated>
<n-tab-pane v-for="(view, index) in views" :key="index" :name="view.name" :tab="view.name">
<n-tag :type="view.tag.type" class="tag-margin">
{{ view.tag.label }}
<template #icon>
<n-icon :component="view.tag.icon" />
</template>
{{ view.path }}
</n-tag>
<n-space justify="space-between">
<n-tag :type="view.tag.type" class="tag-margin">
{{ view.tag.label }}
<template #icon>
<n-icon :component="view.tag.icon" />
</template>
{{ view.path }}
</n-tag>
<n-button type="primary" size="small" class="tag-margin" @click="handleCopy(view.content)"
>复制本页代码</n-button
>
</n-space>
<n-scrollbar class="code-scrollbar" trigger="none">
<n-code :code="view.content" :hljs="hljs" language="goLang" show-line-numbers />
<n-code
:class="'code-' + getFileExtension(view.path)"
:code="view.content"
:hljs="hljs"
:language="getFileExtension(view.path)"
show-line-numbers
/>
</n-scrollbar>
</n-tab-pane>
</n-tabs>
@@ -18,9 +30,12 @@
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { computed, ref } from 'vue';
import hljs from 'highlight.js/lib/core';
import goLang from 'highlight.js/lib/languages/go';
import go from 'highlight.js/lib/languages/go';
import typescript from 'highlight.js/lib/languages/typescript';
import xml from 'highlight.js/lib/languages/xml';
import sql from 'highlight.js/lib/languages/sql';
import { cloneDeep } from 'lodash-es';
import {
CheckmarkCircle,
@@ -29,8 +44,12 @@
HelpCircleOutline,
RemoveCircleOutline,
} from '@vicons/ionicons5';
import { useMessage } from 'naive-ui';
hljs.registerLanguage('goLang', goLang);
hljs.registerLanguage('go', go);
hljs.registerLanguage('ts', typescript);
hljs.registerLanguage('sql', sql);
hljs.registerLanguage('vue', xml);
interface Props {
previewModel: any;
@@ -41,7 +60,8 @@
previewModel: cloneDeep({ views: {} }),
showModal: false,
});
const message = useMessage();
const content = ref('');
const views = computed(() => {
let tmpViews: any = [];
let i = 0;
@@ -69,12 +89,31 @@
}
return tmpViews;
});
function getFileExtension(path: string): string {
const parts = path.split('.');
if (parts.length > 1) {
return parts[parts.length - 1];
}
return '';
}
function handleCopy(code: string) {
content.value = code;
setTimeout(function () {
const copyVal = document.getElementById('copy-code');
copyVal.select();
document.execCommand('copy');
message.success('已复制');
}, 20);
}
</script>
<style lang="less" scoped>
::v-deep(.alert-margin) {
margin-bottom: 20px;
}
::v-deep(.tag-margin) {
margin-bottom: 10px;
}
@@ -85,4 +124,38 @@
color: #e0e2e4;
padding: 10px;
}
::v-deep(.code-vue .hljs-tag) {
color: rgb(242, 197, 92);
}
::v-deep(.code-vue .hljs-name) {
color: rgb(242, 197, 92);
}
::v-deep(.code-vue .hljs-attr) {
color: rgb(49, 104, 213);
}
::v-deep(.code-go .hljs-params) {
color: rgb(49, 104, 213);
}
::v-deep(.code-ts .hljs-params) {
color: rgb(49, 104, 213);
}
::v-deep(.code-ts .hljs-property) {
color: rgb(49, 104, 213);
}
::v-deep(.code-ts .hljs-function) {
color: rgb(49, 104, 213);
}
#copy-code {
position: fixed;
top: -100px;
left: -100px;
}
</style>

View File

@@ -0,0 +1,133 @@
<template>
<div>
<n-modal
title="设置选项字段"
v-model:show="showModal"
:block-scroll="false"
:mask-closable="false"
:show-icon="false"
preset="dialog"
>
<n-form
:model="formValue"
:rules="rules"
ref="formRef"
label-placement="left"
:label-width="100"
class="py-4"
>
<n-form-item label="选项值" path="valueColumn">
<n-select
filterable
tag
placeholder="请选择"
:options="columnsOption"
v-model:value="formValue.valueColumn"
/>
</n-form-item>
<n-form-item label="选项名称" path="labelColumn">
<n-select
filterable
tag
placeholder="请选择"
:options="columnsOption"
v-model:value="formValue.labelColumn"
/>
</n-form-item>
</n-form>
<template #action>
<n-space>
<n-button @click="closeForm">取消</n-button>
<n-button type="info" @click="confirmForm">保存</n-button>
</n-space>
</template>
</n-modal>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { rules } from '@/views/addons/stock/itemBrand/model';
import { cloneDeep } from 'lodash-es';
import { useMessage } from 'naive-ui';
import { Edit } from '@/api/addons/stock/itemClass';
interface Props {
columnsOption: any;
}
const props = withDefaults(defineProps<Props>(), {
columnsOption: [],
});
const rules = {
valueColumn: {
required: true,
trigger: ['blur', 'input'],
type: 'string',
message: '选项值不能为空',
},
labelColumn: {
required: true,
trigger: ['blur', 'input'],
type: 'string',
message: '选项名称不能为空',
},
};
const message = useMessage();
const emit = defineEmits(['update']);
const showModal = ref(false);
const formRef = ref();
const formValue = ref({
valueColumn: null,
labelColumn: null,
});
function openModal(state) {
showModal.value = true;
if (!state) {
state = {
valueColumn: null,
labelColumn: null,
};
}
if (!state.valueColumn) {
const item = props.columnsOption.find((item) => item.value === 'id');
if (item) {
state.valueColumn = item.value;
}
}
if (!state.labelColumn) {
const item = props.columnsOption.find((item) => item.value === 'title' || item.value === 'name');
if (item) {
state.labelColumn = item.value;
}
}
formValue.value = cloneDeep(state);
emit('update', formValue.value);
}
function closeForm() {
showModal.value = false;
}
function confirmForm(e) {
e.preventDefault();
formRef.value.validate((errors) => {
if (!errors) {
emit('update', formValue.value);
closeForm();
} else {
message.error('请填写完整信息');
}
});
}
defineExpose({
openModal,
});
</script>

View File

@@ -1,4 +1,5 @@
import { cloneDeep } from 'lodash-es';
import { isJsonString } from '@/utils/is';
export const genFileObj = {
meth: 1,
@@ -24,7 +25,7 @@ export const genInfoObj = {
varName: '',
options: {
headOps: ['add', 'batchDel', 'export'],
columnOps: ['edit', 'del', 'view', 'status', 'switch', 'check'],
columnOps: ['edit', 'del', 'view', 'status', 'check'],
autoOps: ['genMenuPermissions', 'runDao', 'runService'],
join: [],
menu: {
@@ -32,6 +33,17 @@ export const genInfoObj = {
icon: 'MenuOutlined',
sort: 0,
},
tree: {
titleColumn: null,
styleType: 1,
},
funcDict: {
valueColumn: null,
labelColumn: null,
},
presetStep: {
formGridCols: 1,
},
},
dbName: '',
tableName: '',
@@ -54,6 +66,8 @@ export const selectListObj = {
dictMode: [],
whereMode: [],
buildMeth: [],
tableAlign: [],
treeStyleType: [],
};
export function newState(state) {
@@ -62,3 +76,67 @@ export function newState(state) {
}
return cloneDeep(genInfoObj);
}
export const formGridColsOptions = [
{
value: 1,
label: '一行一列',
},
{
value: 2,
label: '一行两列',
},
{
value: 3,
label: '一行三列',
},
{
value: 4,
label: '一行四列',
},
];
export const formGridSpanOptions = [
{
value: 1,
label: '占一列位置',
},
{
value: 2,
label: '占两列位置',
},
{
value: 3,
label: '占三列位置',
},
{
value: 4,
label: '占四列位置',
},
];
// 格式化列字段
export function formatColumns(columns: any) {
if (columns === undefined || columns.length === 0) {
columns = [];
}
if (isJsonString(columns)) {
columns = JSON.parse(columns);
}
if (columns.length > 0) {
for (let i = 0; i < columns.length; i++) {
if (!columns[i].formGridSpan) {
columns[i].formGridSpan = 1;
}
if (!columns[i].align) {
columns[i].align = 'left';
}
if (!columns[i].width || columns[i].width < 1) {
columns[i].width = null;
}
}
}
return columns;
}

View File

@@ -40,7 +40,10 @@
<template #suffix>
<n-space>
<n-button type="primary" @click="preview">预览代码</n-button>
<n-button type="default" @click="handleBack">返回列表</n-button>
<n-button type="primary" :loading="formBtnPreviewLoading" @click="preview"
>预览代码</n-button
>
<n-button type="success" :loading="formBtnLoading" @click="submitBuild"
>提交生成</n-button
>
@@ -87,7 +90,7 @@
import EditMasterCell from './components/EditMasterCell.vue';
import EditSlaveCell from './components/EditSlaveCell.vue';
import { Selects, View, Preview, Build, Edit } from '@/api/develop/code';
import { selectListObj, newState } from '@/views/develop/code/components/model';
import { selectListObj, newState, formatColumns } from '@/views/develop/code/components/model';
import PreviewTab from '@/views/develop/code/components/PreviewTab.vue';
import { isJsonString } from '@/utils/is';
@@ -108,6 +111,7 @@
const slavePanels = ref<any>([]);
const showModal = ref(false);
const formBtnLoading = ref(false);
const formBtnPreviewLoading = ref(false);
const previewModel = ref<any>();
const dialog = useDialog();
const notification = useNotification();
@@ -123,15 +127,27 @@
async function getGenInfo() {
let tmp = await View({ id: genId });
// 导入主表数据
tmp.masterColumns = formatColumns(tmp.masterColumns);
// 导入生成选项
if (isJsonString(tmp.options)) {
tmp.options = JSON.parse(tmp.options);
}
if (tmp.masterColumns === undefined || tmp.masterColumns.length === 0) {
tmp.masterColumns = [];
// 预设流程
if (!tmp.options.presetStep) {
tmp.options.presetStep = {
formGridCols: 1,
};
}
if (isJsonString(tmp.masterColumns)) {
tmp.masterColumns = JSON.parse(tmp.masterColumns);
// 树表
if (!tmp.options.tree) {
tmp.options.tree = {
titleColumn: null,
styleType: 1,
};
}
genInfo.value = tmp;
@@ -146,12 +162,12 @@
handleClose('主表字段');
}
if (newVal.options.join !== undefined) {
if (newVal && newVal.options && newVal.options.join !== undefined) {
slavePanels.value = [];
for (let i = 0; i <= newVal.options.join.length; i++) {
if (newVal.options.join[i]?.alias !== undefined && newVal.options.join[i]?.alias !== '') {
for (let i = 0; i < newVal.options.join.length; i++) {
if (newVal.options.join[i]?.alias) {
handleSlaveAdd(
'关联表[ ' + newVal.options.join[i]?.alias + ' ]',
'关联表[ ' + newVal.options.join[i].alias + ' ]',
newVal.options.join[i]
);
}
@@ -195,14 +211,21 @@
selectList.value = await Selects({});
};
async function preview() {
previewModel.value = await Preview(genInfo.value);
showModal.value = true;
function preview() {
formBtnPreviewLoading.value = true;
Preview(genInfo.value)
.then((res) => {
previewModel.value = res;
showModal.value = true;
})
.finally(() => {
formBtnPreviewLoading.value = false;
});
}
function submitBuild() {
dialog.warning({
title: '警告',
dialog.info({
title: '提示',
content: '你确定要提交生成吗?',
positiveText: '确定',
negativeText: '取消',
@@ -216,15 +239,12 @@
formBtnLoading.value = false;
});
},
onNegativeClick: () => {
// message.error('取消');
},
});
}
function submitSave() {
dialog.warning({
title: '警告',
dialog.info({
title: '提示',
content: '你确定要保存生成配置吗?',
positiveText: '确定',
negativeText: '取消',
@@ -233,18 +253,15 @@
message.success('操作成功');
});
},
onNegativeClick: () => {
// message.error('取消');
},
});
}
function buildSuccessNotify() {
let count = 6;
let count = 10;
const n = notification.success({
title: '生成提交成功',
content: `如果你使用的热编译,页面将在 ${count} 秒后自动刷新即可生效。否则请手动重启服务后刷新页面!`,
duration: 6000,
duration: 10000,
closable: false,
onAfterEnter: () => {
const minusCount = () => {
@@ -261,6 +278,18 @@
},
});
}
function handleBack() {
dialog.info({
title: '提示',
content: '你确定要返回生成列表?系统不会主动保存更改',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
router.push({ name: 'develop_code' });
},
});
}
</script>
<style lang="less" scoped>
::v-deep(.alert-margin) {

View File

@@ -20,6 +20,7 @@
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1090"
:resizeHeightOffset="-10000"
>
<template #tableTitle>
<n-button type="primary" @click="addTable">
@@ -30,8 +31,7 @@
</template>
立即生成
</n-button>
&nbsp;
<n-button type="error" @click="batchDelete" :disabled="batchDeleteDisabled">
<n-button type="error" @click="batchDelete" :disabled="batchDeleteDisabled" class="min-left-space">
<template #icon>
<n-icon>
<DeleteOutlined />
@@ -44,6 +44,7 @@
<n-modal
v-model:show="showModal"
:mask-closable="false"
:show-icon="false"
preset="dialog"
title="立即生成"
@@ -302,10 +303,10 @@
};
const actionColumn = reactive({
width: 220,
width: 180,
title: '操作',
key: 'action',
// fixed: 'right',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',

View File

@@ -156,7 +156,7 @@
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { computed, ref } from 'vue';
import { useMessage } from 'naive-ui';
import { useRouter, useRoute } from 'vue-router';
import { useSendCode } from '@/hooks/common';
@@ -173,7 +173,6 @@
const { sendLabel, isCounting, loading: sendLoading, activateSend } = useSendCode();
const userStore = useUserStore();
const dialogWidth = ref('75%');
const rules = {
basicName: {
required: true,
@@ -191,6 +190,9 @@
oldPassword: '',
newPassword: '',
});
const dialogWidth = computed(() => {
return adaModalWidth(580);
});
function formSubmit() {
formRef.value.validate((errors) => {
@@ -310,8 +312,4 @@
function sendEmailCode() {
activateSend(SendBindEmail());
}
onMounted(async () => {
adaModalWidth(dialogWidth, 580);
});
</script>

View File

@@ -140,7 +140,7 @@
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { computed, ref } from 'vue';
import { useMessage } from 'naive-ui';
import { useRouter, useRoute } from 'vue-router';
import { useSendCode } from '@/hooks/common';
@@ -157,7 +157,6 @@
const { sendLabel, isCounting, loading: sendLoading, activateSend } = useSendCode();
const userStore = useUserStore();
const dialogWidth = ref('75%');
const rules = {
basicName: {
required: true,
@@ -175,6 +174,9 @@
oldPassword: '',
newPassword: '',
});
const dialogWidth = computed(() => {
return adaModalWidth(580);
});
function formSubmit() {
formRef.value.validate((errors) => {
@@ -212,9 +214,9 @@
function openUpdatePassForm() {
message.error('未开放');
return;
showModal.value = true;
formValue.value.newPassword = '';
formValue.value.oldPassword = '';
// showModal.value = true;
// formValue.value.newPassword = '';
// formValue.value.oldPassword = '';
}
const formMobileBtnLoading = ref(false);
@@ -250,9 +252,9 @@
function openUpdateMobileForm() {
message.error('未开放');
return;
showMobileModal.value = true;
formMobileValue.value.mobile = '';
formMobileValue.value.code = '';
// showMobileModal.value = true;
// formMobileValue.value.mobile = '';
// formMobileValue.value.code = '';
}
const formEmailBtnLoading = ref(false);
@@ -298,8 +300,4 @@
function sendEmailCode() {
activateSend(SendBindEmail());
}
onMounted(async () => {
adaModalWidth(dialogWidth, 580);
});
</script>

View File

@@ -52,9 +52,9 @@
{
field: 'member_id',
component: 'NInput',
label: '操作人',
label: '操作人',
componentProps: {
placeholder: '请输入操作人ID',
placeholder: '请输入操作人ID',
onInput: (e: any) => {
console.log(e);
},

View File

@@ -20,7 +20,7 @@
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1280"
:scroll-x="scrollX"
:resizeHeightOffset="-20000"
>
<template #tableTitle>
@@ -39,7 +39,7 @@
</template>
<script lang="ts" setup>
import { h, reactive, ref } from 'vue';
import { computed, 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';
@@ -47,6 +47,7 @@
import { columns } from './columns';
import { useRouter } from 'vue-router';
import { DeleteOutlined } from '@vicons/antd';
import { adaTableScrollX } from '@/utils/hotgo';
const dialog = useDialog();
const batchDeleteDisabled = ref(true);
@@ -56,9 +57,9 @@
{
field: 'member_id',
component: 'NInput',
label: '操作人',
label: '操作人',
componentProps: {
placeholder: '请输入操作人ID',
placeholder: '请输入操作人ID',
onInput: (e: any) => {
console.log(e);
},
@@ -183,7 +184,7 @@
});
const actionColumn = reactive({
width: 150,
width: 160,
title: '操作',
key: 'action',
fixed: 'right',
@@ -204,6 +205,10 @@
},
});
const scrollX = computed(() => {
return adaTableScrollX(columns, actionColumn.width);
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,

View File

@@ -1,106 +1,122 @@
<template>
<div>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
:title="data.id ? '日志详情 ID' + data.id : '日志详情'"
>
<n-descriptions label-placement="left" class="py-2">
<n-descriptions-item label="请求方式">{{ data.method }}</n-descriptions-item>
<n-descriptions-item>
<template #label>请求地址</template>
{{ data.url }}
</n-descriptions-item>
<n-descriptions-item label="请求耗时">{{ data.takeUpTime }} ms</n-descriptions-item>
<n-descriptions-item label="访问IP">{{ data.ip }}</n-descriptions-item>
<n-descriptions-item label="IP归属地">{{ data.cityLabel }}</n-descriptions-item>
<n-descriptions-item label="链路ID">{{ data.reqId }}</n-descriptions-item>
<n-descriptions-item label="响应时间">{{
timestampToTime(data.timestamp)
}}</n-descriptions-item>
<n-spin :show="loading" description="请稍候...">
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
:title="data.id ? '日志详情 ID' + data.id : '日志详情'"
>
<n-descriptions label-placement="left" class="py-2">
<n-descriptions-item label="请求方式">{{ data.method }}</n-descriptions-item>
<n-descriptions-item>
<template #label>请求地址</template>
{{ data.url }}
</n-descriptions-item>
<n-descriptions-item label="请求耗时">{{ data.takeUpTime }} ms</n-descriptions-item>
<n-descriptions-item label="访问IP">{{ data.ip }}</n-descriptions-item>
<n-descriptions-item label="IP归属地">{{ data.cityLabel }}</n-descriptions-item>
<n-descriptions-item label="链路ID">{{ data.reqId }}</n-descriptions-item>
<n-descriptions-item label="响应时间">{{
timestampToTime(data.timestamp)
}}</n-descriptions-item>
<n-descriptions-item label="创建时间">{{ data.createdAt }}</n-descriptions-item>
</n-descriptions>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="访问代理"
>
{{ data.userAgent }}
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="报错信息"
>
<n-descriptions label-placement="left" class="py-2">
<n-descriptions-item label="错误状态码"> {{ data.errorCode }} </n-descriptions-item>
<n-descriptions-item label="错误提示">
<n-tag type="error"> {{ data.errorMsg }} </n-tag>
</n-descriptions-item>
</n-descriptions>
</n-card>
<n-descriptions-item label="创建时间">{{ data.createdAt }}</n-descriptions-item>
</n-descriptions>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="访问代理"
>
{{ data.userAgent }}
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="报错信息"
>
<n-descriptions label-placement="left" class="py-2">
<n-descriptions-item label="错误状态码"> {{ data.errorCode }} </n-descriptions-item>
<n-descriptions-item label="错误提示">
<n-tag type="error"> {{ data.errorMsg }} </n-tag>
</n-descriptions-item>
</n-descriptions>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="堆栈打印"
>
<JsonViewer
:value="data.errorData"
:expand-depth="5"
copyable
boxed
sort
class="json-width"
/>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="堆栈打印"
>
<JsonViewer
:value="data.errorData"
:expand-depth="5"
copyable
boxed
sort
class="json-width"
/>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="Header请求头"
>
<JsonViewer
:value="data.headerData"
:expand-depth="5"
copyable
boxed
sort
class="json-width"
/>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="Header请求头"
>
<JsonViewer
:value="data.headerData"
:expand-depth="5"
copyable
boxed
sort
class="json-width"
/>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="GET参数"
>
<JsonViewer :value="data.getData" :expand-depth="5" copyable boxed sort class="json-width" />
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="GET参数"
>
<JsonViewer
:value="data.getData"
:expand-depth="5"
copyable
boxed
sort
class="json-width"
/>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="POST参数"
>
<JsonViewer :value="data.postData" :expand-depth="5" copyable boxed sort class="json-width" />
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="POST参数"
>
<JsonViewer
:value="data.postData"
:expand-depth="5"
copyable
boxed
sort
class="json-width"
/>
</n-card>
</n-spin>
</div>
</template>
@@ -116,20 +132,27 @@
const message = useMessage();
const router = useRouter();
const logId = Number(router.currentRoute.value.params.id);
const loading = ref(false);
onMounted(async () => {
onMounted(() => {
if (logId === undefined || logId < 1) {
message.error('ID不正确请检查');
return;
}
await getInfo();
getInfo();
});
const data = ref({});
const getInfo = async () => {
data.value = await View({ id: logId });
const getInfo = () => {
loading.value = true;
View({ id: logId })
.then((res) => {
data.value = res;
})
.finally(() => {
loading.value = false;
});
};
</script>

View File

@@ -24,7 +24,7 @@
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1280"
:scroll-x="scrollX"
:resizeHeightOffset="-20000"
>
<template #tableTitle>
@@ -61,16 +61,17 @@
</div>
</template>
<script lang="ts" setup name="login_log_index">
import { h, reactive, ref } from 'vue';
<script lang="ts" setup>
import { computed, h, onMounted, reactive, ref } 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 { List, Export, Delete } from '@/api/loginLog';
import { columns, schemas } from './model';
import { columns, schemas, loadOptions } from './model';
import { ExportOutlined, DeleteOutlined } from '@vicons/antd';
import { useRouter } from 'vue-router';
import { adaTableScrollX } from '@/utils/hotgo';
const { hasPermission } = usePermission();
const router = useRouter();
@@ -94,6 +95,9 @@
label: '查看详情',
onClick: handleView.bind(null, record),
auth: ['/loginLog/view'],
ifShow: () => {
return record.sysLogId > 0;
},
},
{
label: '删除',
@@ -105,6 +109,10 @@
},
});
const scrollX = computed(() => {
return adaTableScrollX(columns, actionColumn.width);
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
@@ -140,9 +148,6 @@
reloadTable();
});
},
onNegativeClick: () => {
// message.error('取消');
},
});
}
@@ -158,9 +163,6 @@
reloadTable();
});
},
onNegativeClick: () => {
// message.error('取消');
},
});
}
@@ -168,6 +170,10 @@
message.loading('正在导出列表...', { duration: 1200 });
Export(searchFormRef.value?.formModel);
}
onMounted(() => {
loadOptions();
});
</script>
<style lang="less" scoped></style>

View File

@@ -4,8 +4,8 @@ import { cloneDeep } from 'lodash-es';
import { FormSchema } from '@/components/Form';
import { isNullObject } from '@/utils/is';
import { defRangeShortcuts } from '@/utils/dateUtil';
import { getOptionLabel, getOptionTag, Options } from '@/utils/hotgo';
import { loginStatusOptions } from '@/enums/optionsiEnum';
import { getOptionLabel, getOptionTag, Option } from '@/utils/hotgo';
import { Dicts } from '@/api/dict/dict';
export interface State {
id: number;
@@ -40,10 +40,6 @@ export function newState(state: State | null): State {
return cloneDeep(defaultState);
}
export const options = ref<Options>({
sys_normal_disable: [],
});
export const rules = {};
export const schemas = ref<FormSchema[]>([
@@ -59,7 +55,7 @@ export const schemas = ref<FormSchema[]>([
},
},
{
field: 'sysLogIp',
field: 'loginIp',
component: 'NInput',
label: 'IP地址',
componentProps: {
@@ -76,7 +72,7 @@ export const schemas = ref<FormSchema[]>([
defaultValue: null,
componentProps: {
placeholder: '请选择状态',
options: loginStatusOptions,
options: [],
onUpdateValue: (e: any) => {
console.log(e);
},
@@ -125,7 +121,7 @@ export const columns = [
},
{
title: '登录IP',
key: 'sysLogIp',
key: 'loginIp',
width: 160,
},
{
@@ -156,11 +152,11 @@ export const columns = [
style: {
marginRight: '6px',
},
type: getOptionTag(loginStatusOptions, row.status),
type: getOptionTag(options.value.sys_login_status, row.status),
bordered: false,
},
{
default: () => getOptionLabel(loginStatusOptions, row.status),
default: () => getOptionLabel(options.value.sys_login_status, row.status),
}
);
},
@@ -187,3 +183,24 @@ export const columns = [
width: 180,
},
];
// 字典数据选项
export const options = ref({
sys_login_status: [] as Option[],
});
// 加载字典数据选项
export function loadOptions() {
Dicts({
types: ['sys_login_status'],
}).then((res) => {
options.value = res;
for (const item of schemas.value) {
switch (item.field) {
case 'status':
item.componentProps.options = options.value.sys_login_status;
break;
}
}
});
}

View File

@@ -1,136 +0,0 @@
<template>
<div>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
:title="data.id ? '日志详情 ID' + data.id : '日志详情'"
>
<n-descriptions label-placement="left" class="py-2">
<n-descriptions-item label="请求方式">{{ data.method }}</n-descriptions-item>
<n-descriptions-item>
<template #label>请求地址</template>
{{ data.url }}
</n-descriptions-item>
<n-descriptions-item label="请求耗时">{{ data.takeUpTime }} ms</n-descriptions-item>
<n-descriptions-item label="访问IP">{{ data.ip }}</n-descriptions-item>
<n-descriptions-item label="IP归属地">河南 郑州</n-descriptions-item>
<n-descriptions-item label="链路ID">{{ data.reqId }}</n-descriptions-item>
<n-descriptions-item label="响应时间">{{
timestampToTime(data.timestamp)
}}</n-descriptions-item>
<n-descriptions-item label="创建时间">{{ data.createdAt }}</n-descriptions-item>
</n-descriptions>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="访问代理"
>
{{ data.userAgent }}
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="报错信息"
>
<n-descriptions label-placement="left" class="py-2">
<n-descriptions-item label="报错状态码"> {{ data.errorCode }} </n-descriptions-item>
<n-descriptions-item label="报错消息">
<n-tag type="success"> {{ data.errorMsg }} </n-tag>
</n-descriptions-item>
<n-descriptions-item label="报错日志">
<n-tag type="success"> {{ data.errorData }} </n-tag>
</n-descriptions-item>
</n-descriptions>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="Header请求头"
>
<JsonViewer
:value="JSON.parse(data.headerData ?? '{}')"
:expand-depth="5"
copyable
boxed
sort
style="width: 100%; min-width: 3.125rem"
/>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="GET参数"
>
<JsonViewer
:value="JSON.parse(data.getData ?? '{}')"
:expand-depth="5"
copyable
boxed
sort
style="width: 100%; min-width: 3.125rem"
/>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="POST参数"
>
<JsonViewer
:value="JSON.parse(data.postData ?? '{}')"
:expand-depth="5"
copyable
boxed
sort
style="width: 100%; min-width: 3.125rem"
/>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { JsonViewer } from 'vue3-json-viewer';
import 'vue3-json-viewer/dist/index.css';
import { useRouter } from 'vue-router';
import { useMessage } from 'naive-ui';
import { View } from '@/api/log/log';
import { timestampToTime } from '@/utils/dateUtil';
const message = useMessage();
const router = useRouter();
const logId = Number(router.currentRoute.value.params.id);
onMounted(async () => {
if (logId === undefined || logId < 1) {
message.error('ID不正确请检查');
return;
}
await getInfo();
});
const data = ref({});
const getInfo = async () => {
data.value = await View({ id: logId });
};
</script>
<style lang="less" scoped></style>

View File

@@ -25,7 +25,8 @@
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1280"
:scroll-x="scrollX"
:resizeHeightOffset="-20000"
>
<template #tableTitle>
<n-button type="error" @click="batchDelete" :disabled="batchDeleteDisabled">
@@ -43,15 +44,15 @@
</template>
<script lang="ts" setup>
import { h, reactive, ref } from 'vue';
import { computed, h, reactive, ref } from 'vue';
import { NTag, useDialog, useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
import { getLogList, Delete } from '@/api/log/smslog';
import { DeleteOutlined } from '@vicons/antd';
import { Dicts } from '@/api/dict/dict';
import { getOptionLabel, getOptionTag, Options } from '@/utils/hotgo';
import { defRangeShortcuts } from "@/utils/dateUtil";
import { adaTableScrollX, getOptionLabel, getOptionTag, Options } from '@/utils/hotgo';
import { defRangeShortcuts } from '@/utils/dateUtil';
const options = ref<Options>({
config_sms_template: [],
@@ -235,6 +236,10 @@
},
});
const scrollX = computed(() => {
return adaTableScrollX(columns, actionColumn.width);
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,

View File

@@ -11,6 +11,9 @@
{{ item.label }}
</n-button>
</n-space>
<n-space justify="center" class="mt-2">
<n-text depth="3">SaaS系统多租户多应用设计</n-text>
</n-space>
</n-space>
</template>
@@ -23,7 +26,7 @@
const accounts = [
{
label: '超级管理员',
label: '超',
username: 'admin',
password: '123456',
},
@@ -33,10 +36,20 @@
password: '123456',
},
{
label: '代理商',
label: '租户',
username: 'ameng',
password: '123456',
},
{
label: '商户',
username: 'abai',
password: '123456',
},
{
label: '用户',
username: 'asong',
password: '123456',
},
];
function login(username: string, password: string) {

View File

@@ -292,7 +292,9 @@
message.success('登录成功,即将进入系统');
if (route.name === LOGIN_NAME) {
await router.replace('/');
} else await router.replace(toPath);
} else {
await router.replace(toPath);
}
} else {
message.destroyAll();
message.info(msg || '登录失败');

View File

@@ -140,7 +140,7 @@
<script lang="ts" setup>
import '../components/style.less';
import { ref, onMounted } from 'vue';
import { ref, onMounted, computed } from 'vue';
import { useRouter } from 'vue-router';
import { useMessage } from 'naive-ui';
import { ResultEnum } from '@/enums/httpEnum';
@@ -165,6 +165,7 @@
password: string;
}
const emit = defineEmits(['updateActiveModule']);
const formRef = ref();
const router = useRouter();
const message = useMessage();
@@ -176,8 +177,9 @@
const { sendLabel, isCounting, loading: sendLoading, activateSend } = useSendCode();
const agreement = ref(false);
const inviteCodeDisabled = ref(false);
const dialogWidth = ref('85%');
const emit = defineEmits(['updateActiveModule']);
const dialogWidth = computed(() => {
return adaModalWidth();
});
const formInline = ref<FormState>({
username: '',
@@ -243,8 +245,6 @@
inviteCodeDisabled.value = true;
formInline.value.inviteCode = inviteCode;
}
adaModalWidth(dialogWidth);
});
function updateActiveModule(key: string) {

View File

@@ -54,7 +54,7 @@ export const columns = [
{
title: '登录地址',
key: 'addr',
width: 120,
width: 150,
},
{
title(_column) {

View File

@@ -19,7 +19,8 @@
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
:scroll-x="1280"
:scroll-x="scrollX"
:resizeHeightOffset="-10000"
>
<template #tableTitle>
<n-button type="info" @click="openGroupModal">
@@ -46,7 +47,7 @@
</template>
<script lang="ts" setup>
import { h, reactive, ref } from 'vue';
import { computed, 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';
@@ -57,6 +58,7 @@
import Edit from '@/views/monitor/netconn/modal/edit.vue';
import { newState, options, State } from '@/views/monitor/netconn/modal/model';
import { defRangeShortcuts } from '@/utils/dateUtil';
import { adaTableScrollX } from '@/utils/hotgo';
const message = useMessage();
const dialog = useDialog();
@@ -66,7 +68,7 @@
const formParams = ref({});
const actionColumn = reactive({
width: 150,
width: 180,
title: '操作',
key: 'action',
fixed: 'right',
@@ -89,6 +91,10 @@
},
});
const scrollX = computed(() => {
return adaTableScrollX(columns, actionColumn.width);
});
const schemas: FormSchema[] = [
{
field: 'name',
@@ -172,9 +178,6 @@
reloadTable();
});
},
onNegativeClick: () => {
// message.error('取消');
},
});
}

View File

@@ -97,7 +97,7 @@
</template>
<script lang="ts" setup>
import { onMounted, ref, computed, watch } from 'vue';
import { ref, computed, watch } from 'vue';
import { Edit, View } from '@/api/serveLicense';
import DatePicker from '@/components/DatePicker/datePicker.vue';
import { rules, options, State, newState } from './model';
@@ -131,8 +131,10 @@
const params = ref<State>(props.formParams);
const message = useMessage();
const formRef = ref<any>({});
const dialogWidth = ref('75%');
const formBtnLoading = ref(false);
const dialogWidth = computed(() => {
return adaModalWidth();
});
function confirmForm(e) {
e.preventDefault();
@@ -153,10 +155,6 @@
});
}
onMounted(async () => {
adaModalWidth(dialogWidth);
});
function closeForm() {
isShowModal.value = false;
}

View File

@@ -22,9 +22,8 @@
:actionColumn="actionColumn"
:checked-row-keys="checkedIds"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1090"
:scroll-x="scrollX"
:resizeHeightOffset="-10000"
size="small"
>
<template #tableTitle>
<n-button
@@ -118,7 +117,7 @@
</template>
<script lang="ts" setup>
import { h, onMounted, reactive, ref } from 'vue';
import { computed, h, reactive, ref } from 'vue';
import { useDialog, useMessage, NTag } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, useForm } from '@/components/Form/index';
@@ -126,7 +125,7 @@
import { Delete, Export, List, Status, AssignRouter } from '@/api/serveLicense';
import { columns, newState, options, schemas, State } from './model';
import { DeleteOutlined, ExportOutlined, PlusOutlined } from '@vicons/antd';
import { adaModalWidth, getOptionLabel } from '@/utils/hotgo';
import { adaModalWidth, adaTableScrollX, getOptionLabel } from '@/utils/hotgo';
import Edit from './edit.vue';
const { hasPermission } = usePermission();
@@ -139,9 +138,11 @@
const showModal = ref(false);
const formParams = ref<State>();
const showRoutesModal = ref(false);
const dialogWidth = ref('75%');
const formBtnLoading = ref(false);
const formRef = ref<any>({});
const dialogWidth = computed(() => {
return adaModalWidth();
});
const actionColumn = reactive({
width: 300,
@@ -188,6 +189,10 @@
},
});
const scrollX = computed(() => {
return adaTableScrollX(columns, actionColumn.width);
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
@@ -233,9 +238,6 @@
reloadTable();
});
},
onNegativeClick: () => {
// message.error('取消');
},
});
}
@@ -253,9 +255,6 @@
reloadTable();
});
},
onNegativeClick: () => {
// message.error('取消');
},
});
}
@@ -341,10 +340,6 @@
}
);
}
onMounted(async () => {
adaModalWidth(dialogWidth);
});
</script>
<style lang="less" scoped></style>

View File

@@ -46,18 +46,31 @@ export const columns = [
key: 'avatar',
width: 80,
render(row) {
return h(NAvatar, {
size: 32,
src: row.avatar,
});
if (row.avatar !== '') {
return h(NAvatar, {
circle: true,
size: 'small',
src: row.avatar,
});
} else {
return h(
NAvatar,
{
circle: true,
size: 'small',
},
{
default: () => row.username.substring(0, 2),
}
);
}
},
},
{
title: '登录IP',
key: 'ip',
width: 120,
width: 150,
},
// {
// title: 'IP地区',
// key: 'region',

View File

@@ -16,20 +16,22 @@
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
:scroll-x="1280"
:scroll-x="scrollX"
:resizeHeightOffset="-10000"
/>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { h, reactive, ref } from 'vue';
import { computed, 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 { OnlineList, Offline } from '@/api/monitor/monitor';
import { columns } from './columns';
import { defRangeShortcuts } from '@/utils/dateUtil';
import { adaTableScrollX } from '@/utils/hotgo';
const schemas: FormSchema[] = [
{
@@ -107,6 +109,10 @@
},
});
const scrollX = computed(() => {
return adaTableScrollX(columns, actionColumn.width);
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
@@ -125,9 +131,6 @@
reloadTable();
});
},
onNegativeClick: () => {
// message.error('取消');
},
});
}

View File

@@ -27,7 +27,7 @@
:actionColumn="actionColumn"
:checked-row-keys="checkedIds"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1280"
:scroll-x="scrollX"
:resizeHeightOffset="-10000"
size="small"
>
@@ -100,7 +100,7 @@
</template>
<script lang="ts" setup>
import { h, reactive, ref } from 'vue';
import { computed, h, reactive, ref } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, useForm } from '@/components/Form/index';
@@ -111,6 +111,7 @@
import { useRouter } from 'vue-router';
import { JsonViewer } from 'vue3-json-viewer';
import 'vue3-json-viewer/dist/index.css';
import { adaTableScrollX } from '@/utils/hotgo';
const { hasPermission } = usePermission();
const router = useRouter();
@@ -123,7 +124,7 @@
const showModal = ref(false);
const actionColumn = reactive({
width: 150,
width: 220,
title: '操作',
key: 'action',
fixed: 'right',
@@ -152,6 +153,10 @@
},
});
const scrollX = computed(() => {
return adaTableScrollX(columns, actionColumn.width);
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,

View File

@@ -0,0 +1,163 @@
<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:2 l:2 xl:2 2xl:2" responsive="screen">
<n-gi span="2">
<n-form-item label="标题" path="title">
<n-input placeholder="请输入标题" v-model:value="formValue.title" />
</n-form-item>
</n-gi>
<n-gi span="2">
<n-form-item label="上级" path="pid">
<n-tree-select
:options="treeOption"
v-model:value="formValue.pid"
key-field="id"
label-field="title"
clearable
filterable
default-expand-all
show-path
/>
</n-form-item>
</n-gi>
<n-gi span="2">
<n-form-item label="测试分类" path="categoryId">
<n-select v-model:value="formValue.categoryId" :options="options.testCategoryOption" />
</n-form-item>
</n-gi>
<n-gi span="2">
<n-form-item label="描述" path="description">
<n-input placeholder="请输入描述" v-model:value="formValue.description" />
</n-form-item>
</n-gi>
<n-gi span="1">
<n-form-item label="排序" path="sort">
<n-input-number placeholder="请输入排序" v-model:value="formValue.sort" />
</n-form-item>
</n-gi>
<n-gi span="1">
<n-form-item label="状态" path="status">
<n-select v-model:value="formValue.status" :options="options.sys_normal_disable" />
</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/normalTreeDemo';
import { options, State, newState, treeOption, loadTreeOption, rules } from './model';
import { useProjectSettingStore } from '@/store/modules/projectSetting';
import { useMessage } from 'naive-ui';
import { adaModalWidth } from '@/utils/hotgo';
const emit = defineEmits(['reloadTable']);
const message = useMessage();
const settingStore = useProjectSettingStore();
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(840);
});
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>

View File

@@ -0,0 +1,202 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="普通树表">
<!-- 这是由系统生成的CURD表格你可以将此行注释改为表格的描述 -->
</n-card>
</div>
<n-card :bordered="false" class="proCard">
<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" :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(['/normalTreeDemo/edit'])">
<template #icon>
<n-icon>
<PlusOutlined />
</n-icon>
</template>
添加
</n-button>
<n-button type="error" @click="handleBatchDelete" class="min-left-space" v-if="hasPermission(['/normalTreeDemo/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 { List, Delete } from '@/api/normalTreeDemo';
import { PlusOutlined, DeleteOutlined, AlignLeftOutlined } from '@vicons/antd';
import { columns, schemas, loadOptions, newState } 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 viewRef = ref();
const editRef = ref();
const checkedIds = ref([]);
const expandedKeys = ref([]);
const allTreeKeys = ref([]);
const actionColumn = reactive({
width: 216,
title: '操作',
key: 'action',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
auth: ['/normalTreeDemo/edit'],
},
{
label: '添加',
onClick: handleAdd.bind(null, record),
auth: ['/normalTreeDemo/edit'],
},
{
label: '删除',
onClick: handleDelete.bind(null, record),
auth: ['/normalTreeDemo/delete'],
},
],
});
},
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
// 加载普通数表数据
const loadDataTable = async (res = {}) => {
const params = { ...(searchFormRef.value?.formModel ?? {}), ...res, pagination: false };
const dataSource = await List(params);
allTreeKeys.value = expandedKeys.value = dataSource.list.map((item) => item.id);
dataSource.list = convertListToTree(dataSource.list, 'id');
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>

View File

@@ -0,0 +1,222 @@
import { h, ref } from 'vue';
import { NTag } from 'naive-ui';
import { cloneDeep } from 'lodash-es';
import { FormSchema } from '@/components/Form';
import { Dicts } from '@/api/dict/dict';
import { isNullObject } from '@/utils/is';
import { defRangeShortcuts } from '@/utils/dateUtil';
import { Option, getOptionLabel, getOptionTag } from '@/utils/hotgo';
import { renderPopoverMemberSumma, MemberSumma } from '@/utils';
import { TreeOption } from '@/api/normalTreeDemo';
export class State {
public title = ''; // 标题
public id = 0; // ID
public pid = 0; // 上级
public level = 1; // 关系树级别
public tree = ''; // 关系树
public categoryId = null; // 测试分类
public description = ''; // 描述
public sort = 0; // 排序
public status = 1; // 状态
public createdBy = 0; // 创建者
public createdBySumma?: null | MemberSumma = null; // 创建者摘要信息
public updatedBy = 0; // 更新者
public createdAt = ''; // 创建时间
public updatedAt = ''; // 修改时间
public deletedAt = ''; // 删除时间
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 = {
title: {
required: true,
trigger: ['blur', 'input'],
type: 'string',
message: '请输入标题',
},
};
// 表格搜索表单
export const schemas = ref<FormSchema[]>([
{
field: 'title',
component: 'NInput',
label: '标题',
componentProps: {
placeholder: '请输入标题',
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'categoryId',
component: 'NSelect',
label: '测试分类',
defaultValue: null,
componentProps: {
placeholder: '请选择测试分类',
options: [],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'status',
component: 'NSelect',
label: '状态',
defaultValue: null,
componentProps: {
placeholder: '请选择状态',
options: [],
onUpdateValue: (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 columns = [
{
title: '标题',
key: 'title',
align: 'left',
width: 200,
},
{
title: '测试分类',
key: 'categoryId',
align: 'left',
width: 100,
render(row) {
if (isNullObject(row.categoryId)) {
return ``;
}
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: getOptionTag(options.value.testCategoryOption, row.categoryId),
bordered: false,
},
{
default: () => getOptionLabel(options.value.testCategoryOption, row.categoryId),
}
);
},
},
{
title: '描述',
key: 'description',
align: 'left',
width: 300,
},
{
title: '状态',
key: 'status',
align: 'left',
width: 150,
render(row) {
if (isNullObject(row.status)) {
return ``;
}
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: getOptionTag(options.value.sys_normal_disable, row.status),
bordered: false,
},
{
default: () => getOptionLabel(options.value.sys_normal_disable, row.status),
}
);
},
},
{
title: '创建者',
key: 'createdBy',
align: 'left',
width: 100,
render(row) {
return renderPopoverMemberSumma(row.createdBySumma);
},
},
{
title: '创建时间',
key: 'createdAt',
align: 'left',
width: 180,
},
];
// 字典数据选项
export const options = ref({
sys_normal_disable: [] as Option[],
testCategoryOption: [] as Option[],
});
// 加载字典数据选项
export function loadOptions() {
Dicts({
types: ['sys_normal_disable', 'testCategoryOption'],
}).then((res) => {
options.value = res;
for (const item of schemas.value) {
switch (item.field) {
case 'status':
item.componentProps.options = options.value.sys_normal_disable;
break;
case 'categoryId':
item.componentProps.options = options.value.testCategoryOption;
break;
}
}
});
}
// 关系树选项
export const treeOption = ref([]);
// 加载关系树选项
export function loadTreeOption() {
TreeOption().then((res) => {
treeOption.value = res;
});
}

View File

@@ -0,0 +1,92 @@
<template>
<div>
<n-drawer v-model:show="showModal" :width="dialogWidth">
<n-drawer-content title="普通树表详情" closable>
<n-spin :show="loading" description="请稍候...">
<n-descriptions label-placement="left" class="py-2" column="1">
<n-descriptions-item>
<template #label>
标题
</template>
{{ formValue.title }}
</n-descriptions-item>
<n-descriptions-item>
<template #label>
上级
</template>
{{ formValue.pid }}
</n-descriptions-item>
<n-descriptions-item label="测试分类">
<n-tag :type="getOptionTag(options.testCategoryOption, formValue?.categoryId)" size="small" class="min-left-space">
{{ getOptionLabel(options.testCategoryOption, formValue?.categoryId) }}
</n-tag>
</n-descriptions-item>
<n-descriptions-item>
<template #label>
描述
</template>
{{ formValue.description }}
</n-descriptions-item>
<n-descriptions-item>
<template #label>
排序
</template>
{{ formValue.sort }}
</n-descriptions-item>
<n-descriptions-item label="状态">
<n-tag :type="getOptionTag(options.sys_normal_disable, formValue?.status)" size="small" class="min-left-space">
{{ getOptionLabel(options.sys_normal_disable, formValue?.status) }}
</n-tag>
</n-descriptions-item>
</n-descriptions>
</n-spin>
</n-drawer-content>
</n-drawer>
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { useMessage } from 'naive-ui';
import { View } from '@/api/normalTreeDemo';
import { State, newState, options } from './model';
import { adaModalWidth, getOptionLabel, getOptionTag } from '@/utils/hotgo';
import { getFileExt } from '@/utils/urlUtils';
const message = useMessage();
const loading = ref(false);
const showModal = ref(false);
const formValue = ref(newState(null));
const dialogWidth = computed(() => {
return adaModalWidth(580);
});
const fileAvatarCSS = computed(() => {
return {
'--n-merged-size': `var(--n-avatar-size-override, 80px)`,
'--n-font-size': `18px`,
};
});
//下载
function download(url: string) {
window.open(url);
}
function openModal(state: State) {
showModal.value = true;
loading.value = true;
View({ id: state.id })
.then((res) => {
formValue.value = res;
})
.finally(() => {
loading.value = false;
});
}
defineExpose({
openModal,
});
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,163 @@
<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:2 l:2 xl:2 2xl:2" responsive="screen">
<n-gi span="2">
<n-form-item label="标题" path="title">
<n-input placeholder="请输入标题" v-model:value="formValue.title" />
</n-form-item>
</n-gi>
<n-gi span="2">
<n-form-item label="上级" path="pid">
<n-tree-select
:options="treeOption"
v-model:value="formValue.pid"
key-field="id"
label-field="title"
clearable
filterable
default-expand-all
show-path
/>
</n-form-item>
</n-gi>
<n-gi span="2">
<n-form-item label="测试分类" path="categoryId">
<n-select v-model:value="formValue.categoryId" :options="options.testCategoryOption" />
</n-form-item>
</n-gi>
<n-gi span="2">
<n-form-item label="描述" path="description">
<n-input type="textarea" placeholder="描述" v-model:value="formValue.description" />
</n-form-item>
</n-gi>
<n-gi span="1">
<n-form-item label="排序" path="sort">
<n-input-number placeholder="请输入排序" v-model:value="formValue.sort" />
</n-form-item>
</n-gi>
<n-gi span="1">
<n-form-item label="状态" path="status">
<n-select v-model:value="formValue.status" :options="options.sys_normal_disable" />
</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/optionTreeDemo';
import { options, State, newState, treeOption, loadTreeOption, rules } from './model';
import { useProjectSettingStore } from '@/store/modules/projectSetting';
import { useMessage } from 'naive-ui';
import { adaModalWidth } from '@/utils/hotgo';
const emit = defineEmits(['reloadTable']);
const message = useMessage();
const settingStore = useProjectSettingStore();
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(840);
});
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>

View File

@@ -0,0 +1,314 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="选项树表">
<!-- 这是由系统生成的CURD表格你可以将此行注释改为表格的描述 -->
</n-card>
</div>
<n-grid class="mt-4" cols="1 s:1 m:1 l:4 xl:4 2xl:4" responsive="screen" :x-gap="12">
<n-gi span="1">
<n-card :segmented="{ content: true }" :bordered="false" size="small">
<template #header>
<n-space>
<n-button type="info" icon-placement="left" @click="addTable" v-if="hasPermission(['/optionTreeDemo/edit'])">
<template #icon>
<div class="flex items-center">
<n-icon size="14">
<PlusOutlined />
</n-icon>
</div>
</template>
添加
</n-button>
<n-button v-if="hasPermission(['/optionTreeDemo/edit'])" type="info" icon-placement="left" @click="handleEdit(selectedState)" :disabled="selectedState.id < 1">
<template #icon>
<div class="flex items-center">
<n-icon size="14">
<EditOutlined />
</n-icon>
</div>
</template>
编辑
</n-button>
<n-button v-if="hasPermission(['/optionTreeDemo/delete'])" type="error" icon-placement="left" @click="handleEdit(selectedState)" :disabled="selectedState.id < 1">
<template #icon>
<div class="flex items-center">
<n-icon size="14">
<DeleteOutlined />
</n-icon>
</div>
</template>
删除
</n-button>
<n-button type="info" icon-placement="left" @click="handleAllExpanded">
{{ expandedKeys.length ? '收起' : '展开' }}
<template #icon>
<div class="flex items-center">
<n-icon size="14">
<AlignLeftOutlined />
</n-icon>
</div>
</template>
</n-button>
</n-space>
</template>
<div class="w-full menu">
<n-input v-model:value="pattern" placeholder="输入名称搜索">
<template #suffix>
<n-icon size="18" class="cursor-pointer">
<SearchOutlined />
</n-icon>
</template>
</n-input>
<div class="py-3 menu-list">
<template v-if="loading">
<div class="flex items-center justify-center py-4">
<n-spin size="medium" />
</div>
</template>
<n-tree v-else show-line block-line cascade virtual-scroll :pattern="pattern" :data="treeOption" :expandedKeys="expandedKeys" style="height: 75vh" key-field="id" label-field="title" @update:selected-keys="handleSelected" @update:expanded-keys="handleOnExpandedKeys" />
</div>
</div>
</n-card>
</n-gi>
<n-gi span="3">
<n-card :bordered="false" class="proCard">
<template #header v-if="selectedState.id > 0">
<n-space>
<n-icon size="18">
<FormOutlined />
</n-icon>
<span>
正在编辑 {{ selectedState.title }}
</span>
</n-space>
</template>
<n-result v-show="selectedState.id < 1" status="info" title="提示" description="请先从列表选择一项后,进行编辑">
<template #footer>
<n-button type="info" icon-placement="left" @click="handleAdd(selectedState)" v-if="hasPermission(['/optionTreeDemo/edit'])">
<template #icon>
<div class="flex items-center">
<n-icon size="14">
<PlusOutlined />
</n-icon>
</div>
</template>
添加
</n-button>
</template>
</n-result>
<BasicForm v-if="selectedState.id > 0" 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 v-if="selectedState.id > 0" ref="actionRef" openChecked :columns="columns" :request="loadDataTable" :row-key="(row) => row.id" :actionColumn="actionColumn" :scroll-x="scrollX" :resizeHeightOffset="-10000" :checked-row-keys="checkedIds" @update:checked-row-keys="handleOnCheckedRow">
<template #tableTitle>
<n-button type="primary" @click="handleAdd(selectedState)" class="min-left-space" v-if="hasPermission(['/optionTreeDemo/edit'])">
<template #icon>
<n-icon>
<PlusOutlined />
</n-icon>
</template>
添加
</n-button>
<n-button type="error" @click="handleBatchDelete" class="min-left-space" v-if="hasPermission(['/optionTreeDemo/delete'])">
<template #icon>
<n-icon>
<DeleteOutlined />
</n-icon>
</template>
批量删除
</n-button>
</template>
</BasicTable>
</n-card>
</n-gi>
</n-grid>
<Edit ref="editRef" @reloadTable="reloadTable" />
</div>
</template>
<script lang="ts" setup>
import { h, reactive, ref, computed, onMounted, unref } 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 { List, Delete, TreeOption } from '@/api/optionTreeDemo';
import { PlusOutlined, EditOutlined, DeleteOutlined, AlignLeftOutlined, FormOutlined, SearchOutlined } from '@vicons/antd';
import { columns, schemas, loadOptions, loadTreeOption, treeOption, State, newState } from './model';
import { adaTableScrollX, getTreeKeys } 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 pattern = ref('');
const selectedState = ref<State>(newState(null));
const loading = ref(false);
const actionColumn = reactive({
width: 144,
title: '操作',
key: 'action',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
auth: ['/optionTreeDemo/edit'],
},
{
label: '删除',
onClick: handleDelete.bind(null, record),
auth: ['/optionTreeDemo/delete'],
},
],
});
},
});
const scrollX = computed(() => {
return adaTableScrollX(columns, actionColumn.width);
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
// 加载选项式树表数据
const loadDataTable = async (res = {}) => {
if (selectedState.value.id < 1) {
return;
}
// 刷新树选项
loadTreeOption();
// 获取选中的下级列表
const params = {
...(searchFormRef.value?.formModel ?? {}),
...res,
pid: selectedState.value.id,
};
return await List(params);
};
// 更新选中的行
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 handleSelected(keys, option) {
if (keys.length) {
selectedState.value = newState(option[0]);
reloadTable();
} else {
selectedState.value = newState(null);
}
}
function handleOnExpandedKeys(keys) {
expandedKeys.value = keys;
}
function handleAllExpanded() {
if (expandedKeys.value.length) {
expandedKeys.value = [];
} else {
expandedKeys.value = getTreeKeys(unref(treeOption), 'id');
}
}
// 首次加载树选项,默认展开全部
function firstLoadTreeOption() {
loading.value = true;
TreeOption().then((res) => {
treeOption.value = res;
expandedKeys.value = getTreeKeys(unref(treeOption), 'id');
loading.value = false;
});
}
onMounted(() => {
loadOptions();
firstLoadTreeOption();
});
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,216 @@
import { h, ref } from 'vue';
import { NTag } from 'naive-ui';
import { cloneDeep } from 'lodash-es';
import { FormSchema } from '@/components/Form';
import { Dicts } from '@/api/dict/dict';
import { isNullObject } from '@/utils/is';
import { defRangeShortcuts } from '@/utils/dateUtil';
import { Option, getOptionLabel, getOptionTag } from '@/utils/hotgo';
import { renderPopoverMemberSumma, MemberSumma } from '@/utils';
import { TreeOption } from '@/api/optionTreeDemo';
export class State {
public title = ''; // 标题
public id = 0; // ID
public pid = 0; // 上级
public level = 1; // 关系树级别
public tree = ''; // 关系树
public categoryId = null; // 测试分类
public description = ''; // 描述
public sort = 0; // 排序
public status = 1; // 状态
public createdBy = 0; // 创建者
public createdBySumma?: null | MemberSumma = null; // 创建者摘要信息
public updatedBy = 0; // 更新者
public createdAt = ''; // 创建时间
public updatedAt = ''; // 修改时间
public deletedAt = ''; // 删除时间
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 = {
title: {
required: true,
trigger: ['blur', 'input'],
type: 'string',
message: '请输入标题',
},
};
// 表格搜索表单
export const schemas = ref<FormSchema[]>([
{
field: 'title',
component: 'NInput',
label: '标题',
componentProps: {
placeholder: '请输入标题',
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'categoryId',
component: 'NSelect',
label: '测试分类',
defaultValue: null,
componentProps: {
placeholder: '请选择测试分类',
options: [],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'status',
component: 'NSelect',
label: '状态',
defaultValue: null,
componentProps: {
placeholder: '请选择状态',
options: [],
onUpdateValue: (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 columns = [
{
title: '标题',
key: 'title',
align: 'left',
width: 100,
},
{
title: '测试分类',
key: 'categoryId',
align: 'left',
width: 100,
render(row) {
if (isNullObject(row.categoryId)) {
return ``;
}
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: getOptionTag(options.value.testCategoryOption, row.categoryId),
bordered: false,
},
{
default: () => getOptionLabel(options.value.testCategoryOption, row.categoryId),
}
);
},
},
{
title: '状态',
key: 'status',
align: 'left',
width: 150,
render(row) {
if (isNullObject(row.status)) {
return ``;
}
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: getOptionTag(options.value.sys_normal_disable, row.status),
bordered: false,
},
{
default: () => getOptionLabel(options.value.sys_normal_disable, row.status),
}
);
},
},
{
title: '创建者',
key: 'createdBy',
align: 'left',
width: 100,
render(row) {
return renderPopoverMemberSumma(row.createdBySumma);
},
},
{
title: '创建时间',
key: 'createdAt',
align: 'left',
width: 180,
},
];
// 字典数据选项
export const options = ref({
sys_normal_disable: [] as Option[],
testCategoryOption: [] as Option[],
});
// 加载字典数据选项
export function loadOptions() {
Dicts({
types: ['sys_normal_disable', 'testCategoryOption'],
}).then((res) => {
options.value = res;
for (const item of schemas.value) {
switch (item.field) {
case 'status':
item.componentProps.options = options.value.sys_normal_disable;
break;
case 'categoryId':
item.componentProps.options = options.value.testCategoryOption;
break;
}
}
});
}
// 关系树选项
export const treeOption = ref([]);
// 加载关系树选项
export function loadTreeOption() {
TreeOption().then((res) => {
treeOption.value = res;
});
}

View File

@@ -0,0 +1,92 @@
<template>
<div>
<n-drawer v-model:show="showModal" :width="dialogWidth">
<n-drawer-content title="选项树表详情" closable>
<n-spin :show="loading" description="请稍候...">
<n-descriptions label-placement="left" class="py-2" column="1">
<n-descriptions-item>
<template #label>
标题
</template>
{{ formValue.title }}
</n-descriptions-item>
<n-descriptions-item>
<template #label>
上级
</template>
{{ formValue.pid }}
</n-descriptions-item>
<n-descriptions-item label="测试分类">
<n-tag :type="getOptionTag(options.testCategoryOption, formValue?.categoryId)" size="small" class="min-left-space">
{{ getOptionLabel(options.testCategoryOption, formValue?.categoryId) }}
</n-tag>
</n-descriptions-item>
<n-descriptions-item>
<template #label>
描述
</template>
{{ formValue.description }}
</n-descriptions-item>
<n-descriptions-item>
<template #label>
排序
</template>
{{ formValue.sort }}
</n-descriptions-item>
<n-descriptions-item label="状态">
<n-tag :type="getOptionTag(options.sys_normal_disable, formValue?.status)" size="small" class="min-left-space">
{{ getOptionLabel(options.sys_normal_disable, formValue?.status) }}
</n-tag>
</n-descriptions-item>
</n-descriptions>
</n-spin>
</n-drawer-content>
</n-drawer>
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { useMessage } from 'naive-ui';
import { View } from '@/api/optionTreeDemo';
import { State, newState, options } from './model';
import { adaModalWidth, getOptionLabel, getOptionTag } from '@/utils/hotgo';
import { getFileExt } from '@/utils/urlUtils';
const message = useMessage();
const loading = ref(false);
const showModal = ref(false);
const formValue = ref(newState(null));
const dialogWidth = computed(() => {
return adaModalWidth(580);
});
const fileAvatarCSS = computed(() => {
return {
'--n-merged-size': `var(--n-avatar-size-override, 80px)`,
'--n-font-size': `18px`,
};
});
//下载
function download(url: string) {
window.open(url);
}
function openModal(state: State) {
showModal.value = true;
loading.value = true;
View({ id: state.id })
.then((res) => {
formValue.value = res;
})
.finally(() => {
loading.value = false;
});
}
defineExpose({
openModal,
});
</script>
<style lang="less" scoped></style>

View File

@@ -1,20 +1,44 @@
<template>
<div>
<n-card :bordered="false" title="部门管理">
<div class="n-layout-page-header">
<n-card :bordered="false" title="部门管理">
<!-- 这是由系统生成的CURD表格你可以将此行注释改为表格的描述 -->
</n-card>
</div>
<n-card :bordered="false" class="proCard">
<BasicForm
ref="searchFormRef"
@register="register"
@submit="handleSubmit"
@reset="handleReset"
@keyup.enter="handleSubmit"
ref="formRef"
@submit="reloadTable"
@reset="reloadTable"
@keyup.enter="reloadTable"
>
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
<n-space vertical :size="12">
<n-space>
<n-button type="primary" @click="addTable">
<BasicTable
ref="actionRef"
openChecked
:columns="columns"
: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 />
@@ -22,175 +46,91 @@
</template>
添加部门
</n-button>
</n-space>
<n-data-table
v-if="data.length > 0 || !loading"
:columns="columns"
:data="data"
:row-key="rowKey"
:loading="loading"
:resizeHeightOffset="-20000"
default-expand-all
/>
</n-space>
<n-modal
v-model:show="showModal"
:show-icon="false"
preset="dialog"
:title="formParams?.id > 0 ? '编辑部门 #' + formParams?.id : '添加部门'"
>
<n-form
:model="formParams"
:rules="rules"
ref="formRef"
label-placement="left"
:label-width="80"
class="py-4"
>
<n-form-item label="上级部门" path="pid">
<n-tree-select
key-field="id"
:options="options"
:default-value="optionsDefaultValue"
:default-expand-all="true"
@update:value="handleUpdateValue"
/>
</n-form-item>
<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="leader">
<n-input placeholder="请输入负责人" v-model:value="formParams.leader" />
</n-form-item>
<n-form-item label="联系电话" path="phone">
<n-input placeholder="请输入联系电话" v-model:value="formParams.phone" />
</n-form-item>
<n-form-item label="邮箱" path="email">
<n-input placeholder="请输入邮箱" v-model:value="formParams.email" />
</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>
<template #action>
<n-space>
<n-button @click="() => (showModal = false)">取消</n-button>
<n-button type="info" :loading="formBtnLoading" @click="confirmForm">确定</n-button>
</n-space>
<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>
</n-modal>
</BasicTable>
</n-card>
<Edit ref="editRef" @reloadTable="reloadTable" />
</div>
</template>
<script lang="ts" setup name="org_dept">
import { h, onMounted, ref } from 'vue';
import { DataTableColumns, NButton, NTag, useDialog, useMessage } from 'naive-ui';
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
import { PlusOutlined } from '@vicons/antd';
import { TableAction } from '@/components/Table';
import { statusActions, statusOptions } from '@/enums/optionsiEnum';
import { Delete, Edit, getDeptList, Status } from '@/api/org/dept';
import { cloneDeep } from 'lodash-es';
import { renderIcon, renderTooltip } from '@/utils';
import { HelpCircleOutline } from '@vicons/ionicons5';
import { defRangeShortcuts } from '@/utils/dateUtil';
<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';
type RowData = {
createdAt: string;
status: number;
name: string;
id: number;
children?: RowData[];
};
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 rules = {
name: {
required: true,
trigger: ['blur', 'input'],
message: '请输入名称',
const actionColumn = reactive({
width: 160,
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'],
},
],
});
},
code: {
required: true,
trigger: ['blur', 'input'],
message: '请输入编码',
},
};
const schemas: 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);
},
},
},
];
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
@@ -198,167 +138,45 @@
schemas,
});
const options = ref<any>([]);
const optionsDefaultValue = ref<any>(null);
const loading = ref(false);
const formRef: any = ref(null);
const message = useMessage();
const dialog = useDialog();
const showModal = ref(false);
const formBtnLoading = ref(false);
let formParams = ref<any>();
const data = ref<any>([]);
const rowKey = (row: RowData) => row.id;
const defaultState = {
id: 0,
pid: 0,
name: '',
code: '',
type: '',
leader: '',
phone: '',
email: '',
sort: 0,
status: 1,
createdAt: '',
updatedAt: '',
// 加载普通数表数据
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;
};
const columns: DataTableColumns<RowData> = [
{
title(_column) {
return renderTooltip(
h(
NButton,
{
strong: true,
size: 'small',
text: true,
iconPlacement: 'right',
},
{ default: () => '部门', icon: renderIcon(HelpCircleOutline) }
),
'支持上下级部门,点击列表中左侧 > 按钮可展开下级部门列表'
);
},
key: 'name',
render(row) {
return h(
NTag,
{
type: 'info',
},
{
default: () => row.name,
}
);
},
width: 200,
},
// {
// title: '部门ID',
// key: 'index',
// width: 100,
// },
{
title: '部门编码',
key: 'code',
width: 100,
},
{
title: '负责人',
key: 'leader',
width: 100,
},
{
title: '联系电话',
key: 'phone',
width: 150,
},
{
title: '邮箱',
key: 'email',
width: 150,
},
{
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: 'createdAt',
width: 150,
render: (rows, _) => {
return rows.createdAt;
},
},
{
title: '操作',
key: 'actions',
width: 220,
fixed: 'right',
render(record: any) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '添加',
onClick: handleAddSub.bind(null, record),
},
{
label: '编辑',
onClick: handleEdit.bind(null, record),
},
{
label: '删除',
onClick: handleDelete.bind(null, record),
},
],
dropDownActions: statusActions,
select: (key) => {
updateStatus(record.id, key);
},
});
},
},
];
// 更新选中的行
function handleOnCheckedRow(rowKeys) {
checkedIds.value = rowKeys;
}
// 重新加载表格数据
function reloadTable() {
actionRef.value?.reload();
}
// 添加数据
function addTable() {
showModal.value = true;
formParams.value = cloneDeep(defaultState);
optionsDefaultValue.value = 0;
editRef.value.openModal(null);
}
function handleAddSub(record: Recordable) {
showModal.value = true;
formParams.value = cloneDeep(defaultState);
optionsDefaultValue.value = record.id;
// 添加树节点下级数据
function handleAdd(record: Recordable) {
const state = newState(null);
state.pid = record.id;
editRef.value.openModal(state);
}
// 编辑数据
function handleEdit(record: Recordable) {
showModal.value = true;
formParams.value = cloneDeep(record);
formParams.value.children = 0;
optionsDefaultValue.value = formParams.value.pid;
editRef.value.openModal(record);
}
// 单个删除
function handleDelete(record: Recordable) {
dialog.warning({
title: '警告',
@@ -366,84 +184,53 @@
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
Delete(record)
.then((_res) => {
message.success('操作成功');
loadDataTable({});
})
.catch((e: Error) => {
message.error(e.message ?? '操作失败');
});
},
onNegativeClick: () => {
// message.error('取消');
Delete(record).then((_res) => {
message.success('删除成功');
reloadTable();
});
},
});
}
function updateStatus(id: any, status: any) {
Status({ id: id, status: status })
.then((_res) => {
message.success('操作成功');
setTimeout(() => {
loadDataTable({});
});
})
.catch((e: Error) => {
message.error(e.message ?? '操作失败');
});
}
function confirmForm(e: { preventDefault: () => void }) {
e.preventDefault();
formBtnLoading.value = true;
formRef.value.validate((errors: any) => {
if (!errors) {
Edit(formParams.value).then((_res) => {
message.success('操作成功');
setTimeout(() => {
showModal.value = false;
loadDataTable({});
});
});
} else {
message.error('请填写完整信息');
}
formBtnLoading.value = false;
});
}
async function handleSubmit(values: Recordable) {
await loadDataTable(values);
}
function handleReset(_values: Recordable) {}
const loadDataTable = async (res: Recordable<any>) => {
loading.value = true;
const tmp = await getDeptList({ ...res, ...formRef.value?.formModel });
data.value = tmp?.list;
if (data.value === undefined || data.value === null) {
data.value = [];
// 批量删除
function handleBatchDelete() {
if (checkedIds.value.length < 1) {
message.error('请至少选择一项要删除的数据');
return;
}
options.value = [
{
index: 0,
id: 0,
label: '顶级部门',
children: data.value,
dialog.warning({
title: '警告',
content: '你确定要批量删除?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
Delete({ id: checkedIds.value }).then((_res) => {
checkedIds.value = [];
message.success('删除成功');
reloadTable();
});
},
];
loading.value = false;
};
onMounted(async () => {
await loadDataTable({});
});
function handleUpdateValue(value: any) {
formParams.value.pid = value;
});
}
// 收起/展开全部树节点
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>

View File

@@ -0,0 +1,192 @@
<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 options.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 options.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 { options, State, newState, treeOption, loadTreeOption, rules } from './model';
import { useProjectSettingStore } from '@/store/modules/projectSetting';
import { useMessage } from 'naive-ui';
import { adaModalWidth } from '@/utils/hotgo';
const emit = defineEmits(['reloadTable']);
const message = useMessage();
const settingStore = useProjectSettingStore();
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>

View File

@@ -0,0 +1,247 @@
import { h, ref } from 'vue';
import { NTag, NButton } from 'naive-ui';
import { cloneDeep } from 'lodash-es';
import { FormSchema } from '@/components/Form';
import { Dicts } from '@/api/dict/dict';
import { defRangeShortcuts } from '@/utils/dateUtil';
import { validate } from '@/utils/validateUtil';
import { Option, getOptionLabel, getOptionTag } from '@/utils/hotgo';
import { renderTooltip, renderIcon } from '@/utils';
import { HelpCircleOutline } from '@vicons/ionicons5';
import { TreeOption } from '@/api/org/dept';
import { isNullObject } from '@/utils/is';
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 = {
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) {
if (isNullObject(row.type)) {
return ``;
}
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: getOptionTag(options.value.deptType, row.type),
bordered: false,
},
{
default: () => getOptionLabel(options.value.deptType, row.type),
}
);
},
},
{
title: '负责人',
key: 'leader',
width: 100,
},
{
title: '联系电话',
key: 'phone',
width: 150,
},
{
title: '状态',
key: 'status',
align: 'left',
width: 80,
render(row) {
if (isNullObject(row.status)) {
return ``;
}
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: getOptionTag(options.value.sys_normal_disable, row.status),
bordered: false,
},
{
default: () => getOptionLabel(options.value.sys_normal_disable, row.status),
}
);
},
},
{
title: '创建时间',
key: 'createdAt',
width: 150,
},
];
// 字典数据选项
export const options = ref({
sys_normal_disable: [] as Option[],
deptType: [] as Option[],
});
// 加载字典数据选项
export function loadOptions() {
Dicts({
types: ['sys_normal_disable', 'deptType'],
}).then((res) => {
options.value = res;
for (const item of schemas.value) {
switch (item.field) {
case 'status':
item.componentProps.options = options.value.sys_normal_disable;
break;
}
}
});
}
// 关系树选项
export const treeOption = ref([]);
// 加载关系树选项
export function loadTreeOption() {
TreeOption().then((res) => {
treeOption.value = res;
});
}

View File

@@ -5,12 +5,12 @@ export const columns = [
{
title: 'ID',
key: 'id',
width: 80,
width: 100,
},
{
title: '岗位',
key: 'name',
width: 100,
width: 200,
render(row) {
return h(
NTag,
@@ -48,17 +48,14 @@ export const columns = [
);
},
},
// {
// title: '排序',
// key: 'sort',
// width: 100,
// },
{
title: '备注',
key: 'sort',
width: 150,
},
{
title: '创建时间',
key: 'createdAt',
width: 150,
render: (rows, _) => {
return rows.createdAt;
},
width: 180,
},
];

View File

@@ -21,6 +21,7 @@
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:resizeHeightOffset="-10000"
:scroll-x="1090"
>
<template #tableTitle>
@@ -96,16 +97,17 @@
</div>
</template>
<script lang="ts" setup name="org_post">
<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, Status } from '@/api/org/post';
import { Delete, Edit, getPostList } from '@/api/org/post';
import { columns } from './columns';
import { DeleteOutlined, PlusOutlined } from '@vicons/antd';
import { statusActions, statusOptions } from '@/enums/optionsiEnum';
import { defRangeShortcuts } from "@/utils/dateUtil";
import { statusOptions } from '@/enums/optionsiEnum';
import { defRangeShortcuts } from '@/utils/dateUtil';
import { cloneDeep } from 'lodash-es';
const params = ref<any>({
pageSize: 10,
@@ -179,25 +181,18 @@
const resetFormParams = {
id: 0,
pid: 0,
name: '',
code: '',
type: '',
leader: '',
phone: '',
email: '',
sort: 0,
name: '',
remark: '',
sort: null,
status: 1,
created_at: '',
updated_at: '',
};
let formParams = ref<any>(resetFormParams);
const formParams = ref<any>(resetFormParams);
const actionColumn = reactive({
width: 220,
width: 150,
title: '操作',
key: 'action',
// fixed: 'right',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
@@ -211,10 +206,6 @@
onClick: handleDelete.bind(null, record),
},
],
dropDownActions: statusActions,
select: (key) => {
updateStatus(record.id, key);
},
});
},
});
@@ -227,7 +218,7 @@
function addTable() {
showModal.value = true;
formParams.value = resetFormParams;
formParams.value = cloneDeep(resetFormParams);
}
const loadDataTable = async (res) => {
@@ -237,7 +228,6 @@
function onCheckedRow(rowKeys) {
console.log(rowKeys);
batchDeleteDisabled.value = rowKeys.length <= 0;
checkedIds.value = rowKeys;
}
@@ -282,9 +272,6 @@
reloadTable();
});
},
onNegativeClick: () => {
// message.error('取消');
},
});
}
@@ -300,9 +287,6 @@
reloadTable();
});
},
onNegativeClick: () => {
// message.error('取消');
},
});
}
@@ -316,15 +300,6 @@
params.value = values;
reloadTable();
}
function updateStatus(id, status) {
Status({ id: id, status: status }).then((_res) => {
message.success('操作成功');
setTimeout(() => {
reloadTable();
});
});
}
</script>
<style lang="less" scoped></style>

View File

@@ -57,7 +57,7 @@
</template>
<script lang="ts" setup>
import { onMounted, ref, computed, watch } from 'vue';
import { ref, computed, watch } from 'vue';
import {
addRules as rules,
addState as State,
@@ -94,8 +94,10 @@
const params = ref<State>(props.formParams);
const message = useMessage();
const formRef = ref<any>({});
const dialogWidth = ref('75%');
const formBtnLoading = ref(false);
const dialogWidth = computed(() => {
return adaModalWidth();
});
function confirmForm(e) {
e.preventDefault();
@@ -116,10 +118,6 @@
});
}
onMounted(async () => {
adaModalWidth(dialogWidth);
});
function closeForm() {
isShowModal.value = false;
}

View File

@@ -94,8 +94,10 @@
const params = ref<State>(props.formParams);
const message = useMessage();
const formRef = ref<any>({});
const dialogWidth = ref('75%');
const formBtnLoading = ref(false);
const dialogWidth = computed(() => {
return adaModalWidth();
});
function confirmForm(e) {
e.preventDefault();
@@ -116,10 +118,6 @@
});
}
onMounted(async () => {
adaModalWidth(dialogWidth);
});
function closeForm() {
isShowModal.value = false;
}

View File

@@ -1,5 +1,5 @@
import { h } from 'vue';
import { NAvatar, NTag } from 'naive-ui';
import { NAvatar, NTag, NText } from 'naive-ui';
import { formatBefore } from '@/utils/dateUtil';
export const columns = [
@@ -17,6 +17,12 @@ export const columns = [
title: '姓名',
key: 'realName',
width: 100,
render(row) {
if (row.realName == '') {
return h(NText, { depth: 3 }, { default: () => '未设置' });
}
return row.realName;
},
},
{
title: '头像',

View File

@@ -20,7 +20,8 @@
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1280"
:scroll-x="1500"
:resizeHeightOffset="-10000"
>
<template #tableTitle>
<n-button
@@ -86,54 +87,11 @@
class="py-4"
>
<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="username">
<n-input placeholder="请输入登录用户名" v-model:value="formParams.username" />
</n-form-item>
</n-gi>
</n-grid>
<n-grid x-gap="24" :cols="2">
<n-gi>
<n-form-item label="绑定角色" path="roleId">
<n-tree-select
key-field="id"
:options="options.role"
:default-value="formParams.roleId"
:default-expand-all="true"
@update:value="handleUpdateRoleValue"
/>
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="所属部门" path="deptId">
<n-tree-select
key-field="id"
:options="options.dept"
:default-value="formParams.deptId"
:default-expand-all="true"
@update:value="handleUpdateDeptValue"
/>
</n-form-item>
</n-gi>
</n-grid>
<n-grid x-gap="24" :cols="2">
<n-gi>
<n-form-item label="绑定岗位" path="postIds">
<n-select
:default-value="formParams.postIds"
multiple
:options="options.post"
@update:value="handleUpdatePostValue"
/>
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="密码" path="password">
<n-input
@@ -144,7 +102,57 @@
</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="options.dept"
: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="options.role"
: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="options.post"
@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">
@@ -163,7 +171,7 @@
<n-form-item label="性别" path="sex">
<n-radio-group v-model:value="formParams.sex" name="sex">
<n-radio-button
v-for="status in sexOptions"
v-for="status in options.sys_user_sex"
:key="status.value"
:value="status.value"
:label="status.label"
@@ -175,7 +183,7 @@
<n-form-item label="状态" path="status">
<n-radio-group v-model:value="formParams.status" name="status">
<n-radio-button
v-for="status in statusOptions"
v-for="status in options.sys_normal_disable"
:key="status.value"
:value="status.value"
:label="status.label"
@@ -229,7 +237,7 @@
</template>
<script lang="ts" setup>
import { h, reactive, ref, onMounted } from 'vue';
import { h, reactive, ref, onMounted, computed } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import { ActionItem, BasicTable, TableAction } from '@/components/Table';
import { BasicForm } from '@/components/Form/index';
@@ -237,7 +245,6 @@
import { columns } from './columns';
import { PlusOutlined, DeleteOutlined } from '@vicons/antd';
import { QrCodeOutline } from '@vicons/ionicons5';
import { sexOptions, statusOptions } from '@/enums/optionsiEnum';
import { adaModalWidth } from '@/utils/hotgo';
import { getRandomString } from '@/utils/charset';
import { cloneDeep } from 'lodash-es';
@@ -279,16 +286,18 @@
const formRef = ref<any>({});
const batchDeleteDisabled = ref(true);
const checkedIds = ref([]);
const dialogWidth = ref('50%');
const formParams = ref<any>();
const showQrModal = ref(false);
const qrParams = ref({
name: '',
qrUrl: '',
});
const dialogWidth = computed(() => {
return adaModalWidth();
});
const actionColumn = reactive({
width: 240,
width: 280,
title: '操作',
key: 'action',
fixed: 'right',
@@ -389,7 +398,6 @@
}
const loadDataTable = async (res) => {
adaModalWidth(dialogWidth);
return await List({ ...res, ...searchFormRef.value?.formModel, ...{ roleId: props.type } });
};

View File

@@ -2,11 +2,10 @@ import { cloneDeep } from 'lodash-es';
import { ref } from 'vue';
import { getDeptOption } from '@/api/org/dept';
import { getRoleOption } from '@/api/system/role';
import { getPostOption } from '@/api/org/post';
import { FormSchema, useForm } from '@/components/Form';
import { statusOptions } from '@/enums/optionsiEnum';
import { defRangeShortcuts } from '@/utils/dateUtil';
import {Dicts} from "@/api/dict/dict";
import { Dicts } from '@/api/dict/dict';
// 增加余额/积分.
@@ -199,6 +198,8 @@ export const options = ref<any>({
roleTabs: [{ id: -1, name: '全部' }],
dept: [],
post: [],
sys_user_sex: [],
sys_normal_disable: [],
});
export async function loadOptions() {
@@ -215,9 +216,11 @@ export async function loadOptions() {
}
const tmpOptions = await Dicts({
types: ['adminPostOption'],
types: ['adminPostOption', 'sys_user_sex', 'sys_normal_disable'],
});
options.value.post =tmpOptions?.adminPostOption;
options.value.post = tmpOptions?.adminPostOption;
options.value.sys_user_sex = tmpOptions?.sys_user_sex;
options.value.sys_normal_disable = tmpOptions?.sys_normal_disable;
}
function treeDataToCompressed(source) {

View File

@@ -1,400 +0,0 @@
<template>
<n-drawer v-model:show="isDrawer" :width="width" :placement="placement" :mask-closable="false">
<n-drawer-content :title="title" closable>
<n-form
:model="formParams"
:rules="rules"
ref="formRef"
label-placement="left"
:label-width="100"
>
<n-divider title-placement="left">基本设置</n-divider>
<n-form-item label="类型" path="type">
<n-radio-group v-model:value="formParams.type" name="type">
<n-radio-button
v-for="menuType in menuTypes"
:key="menuType.value"
:value="menuType.value"
:label="menuType.label"
/>
</n-radio-group>
</n-form-item>
<n-form-item label="上级目录" path="pid">
<n-tree-select
filterable
:options="optionTreeData"
:default-value="formParams.pid"
@update:value="handleUpdateValue"
/>
</n-form-item>
<n-form-item
:label="
formParams.type === 1 ? '目录名称' : formParams.type === 2 ? '菜单名称' : '按钮名称'
"
path="title"
>
<n-input placeholder="请输入" v-model:value="formParams.title" />
</n-form-item>
<n-form-item path="icon" v-if="formParams.type !== 3">
<IconSelector style="width: 100%" v-model:value="formParams.icon" option="antd" />
<template #label>
<n-tooltip trigger="hover">
<template #trigger>
<n-icon :component="QuestionCircleOutlined" :size="18" :depth="3" />
</template>
请填写图标编码可以参考图标库也可以不填使用默认图标
</n-tooltip>
菜单图标</template
>
</n-form-item>
<n-form-item path="path" v-if="formParams.type !== 3">
<n-input placeholder="路由地址" v-model:value="formParams.path" />
<template #label>
<n-tooltip trigger="hover">
<template #trigger>
<n-icon :component="QuestionCircleOutlined" :size="18" :depth="3" />
</template>
请路由地址user
</n-tooltip>
路由地址</template
>
</n-form-item>
<n-form-item path="name">
<n-input placeholder="路由别名" v-model:value="formParams.name" />
<template #label>
<n-tooltip trigger="hover">
<template #trigger>
<n-icon :component="QuestionCircleOutlined" :size="18" :depth="3" />
</template>
对应路由配置文件中 `name` 只能是唯一性配置 `http(s)://` 开头地址 则会新窗口打开
</n-tooltip>
路由别名</template
>
</n-form-item>
<n-form-item label="组件路径" path="component" v-if="formParams.type !== 3">
<n-input placeholder="组件路径" v-model:value="formParams.component" />
<template #feedback>
主目录填 `LAYOUT`;多级父目录填
`ParentLayout`;页面填具体的组件路径`/system/menu/menu`</template
>
</n-form-item>
<n-form-item label="默认跳转" path="redirect" v-if="formParams.type === 1">
<n-input placeholder="默认路由跳转地址" v-model:value="formParams.redirect" />
<template #feedback
>默认跳转路由地址`/system/menu/menu` 多级路由情况下适用</template
>
</n-form-item>
<n-divider title-placement="left">功能设置</n-divider>
<n-form-item label="分配权限" path="permissions">
<n-input
placeholder="请输入分配权限,多个权限用,分割"
v-model:value="formParams.permissions"
/>
<template #label>
<n-tooltip trigger="hover">
<template #trigger>
<n-icon :component="QuestionCircleOutlined" :size="18" :depth="3" />
</template>
请填写API路由地址可同时作用于服务端和web端多个权限用,分割
</n-tooltip>
分配权限</template
>
</n-form-item>
<!-- <n-form-item label="权限名称" path="permissionName">-->
<!-- <n-input placeholder="权限名称" v-model:value="formParams.permissionName" />-->
<!-- </n-form-item>-->
<n-form-item label="高亮路由" path="activeMenu" v-if="formParams.type !== 3">
<n-input placeholder="高亮路由" v-model:value="formParams.activeMenu" />
</n-form-item>
<n-form-item label="菜单排序" path="sort">
<n-input-number style="width: 100%" v-model:value="formParams.sort" clearable />
</n-form-item>
<n-grid x-gap="24" :cols="2" v-if="formParams.type !== 3">
<n-gi>
<n-form-item label="根路由" path="isRoot">
<n-radio-group v-model:value="formParams.isRoot" name="isRoot">
<n-radio-button
v-for="switchStatus in statusMap"
:key="switchStatus.value"
:value="switchStatus.value"
:label="switchStatus.label"
/>
</n-radio-group>
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="页签固定" path="affix">
<n-radio-group v-model:value="formParams.affix" name="affix">
<n-radio-button
v-for="switchStatus in statusMap"
:key="switchStatus.value"
:value="switchStatus.value"
:label="switchStatus.label"
/>
</n-radio-group>
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="简化路由" path="alwaysShow">
<n-radio-group v-model:value="formParams.alwaysShow" name="alwaysShow">
<n-radio-button
v-for="switchStatus in statusMap"
:key="switchStatus.value"
:value="switchStatus.value"
:label="switchStatus.label"
/>
</n-radio-group>
</n-form-item>
</n-gi>
</n-grid>
<n-grid x-gap="24" :cols="2" v-if="formParams.type !== 3">
<n-gi>
<n-form-item label="缓存路由" path="keepAlive">
<n-radio-group v-model:value="formParams.keepAlive" name="keepAlive">
<n-radio-button
v-for="switchStatus in statusMap"
:key="switchStatus.value"
:value="switchStatus.value"
:label="switchStatus.label"
/>
</n-radio-group>
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="是否隐藏" path="hidden">
<n-radio-group v-model:value="formParams.hidden" name="hidden">
<n-radio-button
v-for="hidden in hiddenMap"
:key="hidden.value"
:value="hidden.value"
:label="hidden.label"
/>
</n-radio-group>
</n-form-item>
</n-gi>
</n-grid>
<n-grid x-gap="24" :cols="2" v-if="formParams.type !== 3">
<n-gi>
<n-form-item label="是否外链" path="isFrame">
<n-radio-group v-model:value="formParams.isFrame" name="isFrame">
<n-radio-button
v-for="switchStatus in statusMap"
:key="switchStatus.value"
:value="switchStatus.value"
:label="switchStatus.label"
/>
</n-radio-group>
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="外部地址" path="frameSrc" v-show="formParams.isFrame === 1">
<n-input placeholder="内联外部地址" v-model:value="formParams.frameSrc" />
</n-form-item>
</n-gi>
</n-grid>
<n-form-item label="状态" path="status">
<n-radio-group v-model:value="formParams.status" name="status">
<n-radio-button
v-for="status in statusMap"
:key="status.value"
:value="status.value"
:label="status.label"
/>
</n-radio-group>
</n-form-item>
</n-form>
<template #footer>
<n-space>
<n-button type="primary" :loading="subLoading" @click="formSubmit">确认添加</n-button>
<n-button @click="handleReset">重置</n-button>
<n-button @click="closeDrawer">取消</n-button>
</n-space>
</template>
</n-drawer-content>
</n-drawer>
</template>
<script lang="ts">
import { defineComponent, reactive, ref, toRefs } from 'vue';
import { FormItemRule, TreeSelectOption, useMessage } from 'naive-ui';
import { QuestionCircleOutlined } from '@vicons/antd';
import { EditMenu } from '@/api/system/menu';
import { newState } from '@/views/permission/menu/model';
const menuTypes = [
{
value: 1,
label: '目录',
},
{
value: 2,
label: '菜单',
},
{
value: 3,
label: '按钮',
},
].map((s) => {
return s;
});
const switchStatusMap = [
{
value: 0,
label: '关闭',
},
{
value: 1,
label: '开启',
},
].map((s) => {
return s;
});
const statusMap = [
{
value: 0,
label: '禁用',
},
{
value: 1,
label: '启用',
},
].map((s) => {
return s;
});
const hiddenMap = [
{
value: 0,
label: '否',
},
{
value: 1,
label: '是',
},
].map((s) => {
return s;
});
export default defineComponent({
name: 'CreateDrawer',
components: {},
props: {
title: {
type: String,
default: '添加顶级菜单',
},
optionTreeData: {
type: Object || Array,
default: [],
},
},
emits: ['loadData'],
setup(_props, context) {
const message = useMessage();
const formRef: any = ref(null);
const state = reactive<any>({
width: 700,
isDrawer: false,
subLoading: false,
formParams: newState(null),
placement: 'right',
});
const rules = {
title: {
required: true,
message: '请输入名称',
trigger: 'blur',
},
label: {
required: true,
message: '请输入标题',
trigger: 'blur',
},
path: {
required: false,
message: '请输入路由地址',
trigger: 'blur',
validator: function (_rule: FormItemRule, value: any, callback: Function) {
if (state.formParams.type != 3 && !value) {
callback(new Error('请输入路由地址'));
}
},
},
};
function openDrawer(pid: number) {
if (document.body.clientWidth < 700) {
state.width = document.body.clientWidth;
}
state.isDrawer = true;
state.formParams = newState(null);
state.formParams.pid = pid;
if (pid > 0) {
state.formParams.type = 2;
}
}
function closeDrawer() {
state.isDrawer = false;
}
function formSubmit() {
formRef.value.validate((errors) => {
if (!errors) {
state.subLoading = true;
EditMenu({ ...state.formParams })
.then(async (_res) => {
state.subLoading = false;
message.success('操作成功');
handleReset();
await context.emit('loadData');
closeDrawer();
})
.catch((_e: Error) => {
state.subLoading = false;
});
} else {
message.error('请填写完整信息');
}
});
}
function handleReset() {
formRef.value.restoreValidation();
state.formParams = newState(null);
}
// 处理选项更新
function handleUpdateValue(
value: string | number | Array<string | number> | null,
_option: TreeSelectOption | null | Array<TreeSelectOption | null>
) {
state.formParams.pid = value;
}
return {
...toRefs(state),
formRef,
rules,
formSubmit,
handleReset,
openDrawer,
closeDrawer,
menuTypes,
switchStatusMap,
statusMap,
hiddenMap,
handleUpdateValue,
QuestionCircleOutlined,
};
},
});
</script>

View File

@@ -0,0 +1,84 @@
<template>
<div>
<n-modal
v-model:show="showModal"
:mask-closable="false"
:show-icon="false"
preset="dialog"
transform-origin="center"
title="添加菜单"
:style="{
width: dialogWidth,
}"
>
<n-scrollbar style="max-height: 87vh" class="pr-5">
<n-spin :show="loading" description="请稍候...">
<EditForm
ref="editFormRef"
v-model:formParams="formParams"
v-model:treeOption="treeOption"
@reloadTable="reloadTable"
@closeForm="closeForm"
/>
</n-spin>
</n-scrollbar>
<template #action>
<n-space>
<n-button @click="closeForm"> 关闭 </n-button>
<n-button @click="handleReset"> 重置 </n-button>
<n-button type="primary" :loading="formLoading" @click="formSubmit"> 确定添加 </n-button>
</n-space>
</template>
</n-modal>
</div>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import { State, newState } from './model';
import { adaModalWidth } from '@/utils/hotgo';
import EditForm from '@/views/permission/menu/editForm.vue';
const emit = defineEmits(['reloadTable']);
const formParams = ref<State>(newState(null));
const treeOption = defineModel<any[]>('treeOption');
const loading = ref(false);
const showModal = ref(false);
const editFormRef = ref();
const formLoading = computed(() => {
return editFormRef.value?.getFormLoading();
});
const dialogWidth = computed(() => {
return adaModalWidth(960);
});
function reloadTable() {
emit('reloadTable');
}
function openModal(state: State) {
showModal.value = true;
formParams.value = newState(state);
}
function formSubmit(e) {
e.preventDefault();
editFormRef.value.formSubmit();
}
function handleReset() {
editFormRef.value.handleReset();
}
function closeForm() {
showModal.value = false;
loading.value = false;
}
defineExpose({
openModal,
});
</script>
<style lang="less"></style>

View File

@@ -0,0 +1,417 @@
<template>
<div>
<n-form
:model="formParams"
:rules="rules"
ref="formRef"
label-placement="left"
:label-width="100"
class="py-4"
>
<n-grid cols="2 300:1 600:2">
<n-gi>
<n-form-item label="菜单类型" path="type">
<n-radio-group
v-model:value="formParams.type"
name="type"
:on-update:value="handleUpdateType"
>
<n-radio-button
v-for="menuType in options.sys_menu_types"
:key="menuType.value"
:value="menuType.value"
:label="menuType.label"
/>
</n-radio-group>
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="上级目录" path="pid">
<n-cascader
clearable
filterable
:options="filterTreeOption"
v-model:value="formParams.pid"
value-field="key"
label-field="label"
/>
</n-form-item>
</n-gi>
<n-gi>
<n-form-item :label="titleLabel" path="title">
<n-input placeholder="请输入" v-model:value="formParams.title" />
</n-form-item>
</n-gi>
<n-gi v-if="formParams.type !== 3">
<n-form-item path="icon">
<IconSelector style="width: 100%" v-model:value="formParams.icon" option="antd" />
<template #label>
<n-tooltip trigger="hover">
<template #trigger>
<n-icon :component="QuestionCircleOutlined" :size="18" :depth="3" />
</template>
请填写图标编码可以参考图标库也可以不填使用默认图标
</n-tooltip>
菜单图标
</template>
</n-form-item>
</n-gi>
<n-gi v-if="formParams.type !== 3">
<n-form-item path="path">
<n-input placeholder="路由地址" v-model:value="formParams.path" />
<template #label>
<n-tooltip trigger="hover">
<template #trigger>
<n-icon :component="QuestionCircleOutlined" :size="18" :depth="3" />
</template>
请路由地址user
</n-tooltip>
路由地址
</template>
</n-form-item>
</n-gi>
<n-gi>
<n-form-item path="name">
<n-input placeholder="路由别名" v-model:value="formParams.name" />
<template #label>
<n-tooltip trigger="hover">
<template #trigger>
<n-icon :component="QuestionCircleOutlined" :size="18" :depth="3" />
</template>
对应路由配置文件中 `name` 只能是唯一性配置 `http(s)://` 开头地址 则会新窗口打开
</n-tooltip>
路由别名
</template>
</n-form-item>
</n-gi>
</n-grid>
<n-grid cols="2 300:1 600:2" v-if="formParams.type !== 3">
<n-gi v-if="formParams.type === 1">
<n-form-item label="目录组件" path="component">
<n-select
v-if="formParams.type === 1"
:options="options.sys_menu_component"
v-model:value="formParams.component"
placeholder="请选择目录组件"
clearable
tag
/>
</n-form-item>
</n-gi>
<n-gi v-if="formParams.type === 2">
<n-form-item label="组件路径" path="component">
<n-input placeholder="组件路径" v-model:value="formParams.component" />
<template #feedback> Vue 组件路径`/system/menu/menu` </template>
</n-form-item>
</n-gi>
<n-gi v-if="formParams.type === 1">
<n-form-item label="默认跳转" path="redirect">
<n-input placeholder="默认路由跳转地址" v-model:value="formParams.redirect" />
<template #feedback>当目录下存在多个同级菜单时适用`/system/menu/menu`</template>
</n-form-item>
</n-gi>
</n-grid>
<n-divider title-placement="left">功能设置</n-divider>
<n-grid cols="1">
<n-gi>
<n-form-item label="分配权限" path="permissions">
<n-dynamic-tags
v-model:value="permissions"
:on-update:value="handleUpdatePermissions"
type="success"
/>
<template #label>
<n-tooltip trigger="hover">
<template #trigger>
<n-icon :component="QuestionCircleOutlined" :size="18" :depth="3" />
</template>
请填写API路径地址可同时作用于server端接口鉴权和web端细粒度权限一次添加多个权限用,分割
</n-tooltip>
分配权限
</template>
</n-form-item>
</n-gi>
<!-- <n-gi>-->
<!-- <n-form-item label="权限名称" path="permissionName">-->
<!-- <n-input placeholder="权限名称" v-model:value="formParams.permissionName" />-->
<!-- <template #feedback>分配权限存在多个时权限名称只绑定到第一个权限</template>-->
<!-- </n-form-item>-->
<!-- </n-gi>-->
</n-grid>
<n-grid cols="2 300:1 600:2">
<n-gi v-if="formParams.type !== 3">
<n-form-item label="高亮路由" path="activeMenu">
<n-input placeholder="高亮路由" v-model:value="formParams.activeMenu" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="菜单排序" path="sort">
<n-input-number style="width: 100%" v-model:value="formParams.sort" clearable />
</n-form-item>
</n-gi>
<n-gi v-if="formParams.type !== 3">
<n-form-item label="内嵌链接" path="frameSrc">
<n-input-group>
<n-select
:style="{ width: '33%', minWidth: '80px' }"
:options="options.sys_switch"
v-model:value="formParams.isFrame"
/>
<n-input
placeholder="格式http://www.xxx.cn 或 https://www.xxx.cn"
v-model:value="formParams.frameSrc"
/>
</n-input-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 options.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="senior" v-if="formParams.type !== 3">
<n-checkbox-group v-model:value="senior" :on-update:value="handleSeniorChecked">
<n-space item-style="display: flex;">
<n-checkbox value="keepAlive" label="缓存路由" />
<n-checkbox value="hidden" label="隐藏菜单" />
<n-checkbox value="alwaysShow" label="简化路由" />
<n-checkbox value="affix" label="页签固定" />
<n-checkbox value="isRoot" label="根路由" />
</n-space>
</n-checkbox-group>
</n-form-item>
<n-form-item path="auth" style="margin-left: 100px" v-if="formParams.id > 0">
<n-space>
<n-button type="primary" :loading="loading" @click="formSubmit">保存修改 </n-button>
<n-button @click="handleReset">重置</n-button>
<n-button @click="handleDel">删除</n-button>
</n-space>
</n-form-item>
</n-form>
</div>
</template>
<script setup lang="ts">
import { QuestionCircleOutlined } from '@vicons/antd';
import IconSelector from '@/components/IconSelector/index.vue';
import { computed, ref } from 'vue';
import { newState, State, options } from '@/views/permission/menu/model';
import { FormItemRule, useDialog, useMessage } from 'naive-ui';
import { cloneDeep } from 'lodash-es';
import { DeleteMenu, EditMenu } from '@/api/system/menu';
import { findTreeNode } from '@/utils';
const rules = {
title: {
required: true,
message: '请输入名称',
trigger: 'blur',
},
label: {
required: true,
message: '请输入标题',
trigger: 'blur',
},
path: {
required: false,
message: '请输入路由地址',
trigger: 'blur',
validator: function (_rule: FormItemRule, value: any, callback: Function) {
if (formParams.value.type != 3 && !value) {
callback(new Error('请输入路由地址'));
}
},
},
};
const emit = defineEmits(['reloadTable', 'closeForm']);
const message = useMessage();
const dialog = useDialog();
const loading = ref(false);
const formRef = ref();
const formParams = defineModel<State>('formParams', { required: true });
const treeOption = defineModel<any[]>('treeOption');
const titleLabel = computed(() => {
const type = formParams.value.type as number;
if (type == 1) {
return '目录名称';
}
if (type == 2) {
return '菜单名称';
}
return '按钮名称';
});
const filterTreeOption = computed(() => {
const list = cloneDeep(treeOption.value) as any[];
for (let i = 0; i < list.length; i++) {
const item = list[i];
if (item.id === formParams.value.id) {
item.disabled = true;
if (item.children) {
setChildrenDisabled(item.children);
}
} else {
if (item.children) {
const foundChild = findItemById(item.children, formParams.value.id);
if (foundChild) {
foundChild.disabled = true;
setChildrenDisabled(foundChild.children);
}
}
}
}
return list;
});
const senior = computed(() => {
let opts: string[] = [];
if (formParams.value.keepAlive == 1) {
opts.push('keepAlive');
}
if (formParams.value.hidden == 1) {
opts.push('hidden');
}
if (formParams.value.alwaysShow == 1) {
opts.push('alwaysShow');
}
if (formParams.value.affix == 1) {
opts.push('affix');
}
if (formParams.value.isRoot == 1) {
opts.push('isRoot');
}
return opts;
});
function handleSeniorChecked(_: string, opt: { actionType: 'check' | 'uncheck'; value: string }) {
if (opt.actionType == 'check') {
formParams.value[opt.value] = 1;
} else {
formParams.value[opt.value] = 0;
}
}
function setChildrenDisabled(children: State[]) {
if (!children) return;
for (let i = 0; i < children.length; i++) {
const child = children[i];
child.disabled = true;
if (child.children) {
setChildrenDisabled(child.children);
}
}
}
function findItemById(children: State[], id: number) {
if (!children) {
return null;
}
for (let i = 0; i < children.length; i++) {
const item = children[i];
if (item.id === id) {
return item;
}
if (item.children) {
const child = findItemById(item.children, id);
if (child) {
return child;
}
}
}
return null;
}
function getFormLoading() {
return loading.value;
}
function handleDel() {
dialog.warning({
title: '提示',
content: `您确定要删除 ` + formParams.value.title + ` 菜单吗?`,
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
DeleteMenu(formParams.value).then((_res) => {
message.success('操作成功');
formParams.value = newState(null);
emit('reloadTable');
});
},
});
}
function handleReset() {
const item = findTreeNode(treeOption.value, formParams.value.id);
formParams.value = newState(item);
}
function formSubmit() {
formRef.value.validate((errors: boolean) => {
if (!errors) {
loading.value = true;
EditMenu(formParams.value)
.then((_res) => {
message.success('操作成功');
emit('reloadTable');
emit('closeForm');
})
.finally(() => {
loading.value = false;
});
} else {
message.error('请填写完整信息');
}
});
}
const permissions = computed(() => {
if (formParams.value.permissions.length == 0) {
return [];
}
return formParams.value.permissions.split(',');
});
function handleUpdatePermissions(values: string[]) {
formParams.value.permissions = Array.from(new Set(values)).join(',');
}
function handleUpdateType(value: number) {
formParams.value.type = value;
if (value == 1) {
formParams.value.component = null;
} else {
formParams.value.component = '';
}
const item = findTreeNode(treeOption.value, formParams.value.id);
if (item && item.type == value) {
formParams.value.component = item.component;
}
}
defineExpose({
handleReset,
formSubmit,
getFormLoading,
});
</script>
<style scoped lang="less"></style>

View File

@@ -1,11 +1,16 @@
<template>
<div>
<div class="n-layout-page-header">
<div class="n-layout-page-header" v-if="!isModal">
<n-card :bordered="false" title="菜单管理">
在这里可以管理编辑系统下的所有菜单导航和分配相应的菜单权限
</n-card>
</div>
<n-grid class="mt-4" cols="1 s:1 m:1 l:3 xl:3 2xl:3" responsive="screen" :x-gap="12">
<n-grid
:class="isModal ? '' : 'mt-4'"
cols="1 s:1 m:1 l:3 xl:3 2xl:3"
responsive="screen"
:x-gap="12"
>
<n-gi span="1">
<n-card :segmented="{ content: true }" :bordered="false" size="small">
<template #header>
@@ -24,7 +29,7 @@
type="info"
icon-placement="left"
@click="openChildCreateDrawer"
:disabled="!isEditMenu"
:disabled="formParams.id == 0"
>
<template #icon>
<div class="flex items-center">
@@ -48,7 +53,7 @@
</n-space>
</template>
<div class="w-full menu">
<n-input type="input" v-model:value="pattern" placeholder="输入菜单名称搜索">
<n-input type="text" v-model:value="pattern" placeholder="输入菜单名称搜索">
<template #suffix>
<n-icon size="18" class="cursor-pointer">
<SearchOutlined />
@@ -68,7 +73,7 @@
checkable
:virtual-scroll="true"
:pattern="pattern"
:data="treeData"
:data="treeOption"
:expandedKeys="expandedKeys"
style="max-height: 650px; overflow: hidden"
@update:selected-keys="selectedTree"
@@ -87,540 +92,104 @@
<FormOutlined />
</n-icon>
<span>编辑菜单{{ treeItemTitle ? `${treeItemTitle}` : '' }}</span>
<!-- <span style="font-size: 14px">{{ treeItemTitle }}</span>-->
</n-space>
</template>
<n-result
v-show="!isEditMenu"
v-show="formParams.id == 0"
status="info"
title="提示"
description="从菜单列表中选择一项进行编辑"
/>
<n-form
:model="formParams"
:rules="rules"
ref="formRef"
label-placement="left"
:label-width="100"
v-if="isEditMenu"
class="py-4"
>
<n-divider title-placement="left">基本设置</n-divider>
<n-grid cols="2 300:1 600:2">
<n-gi>
<n-form-item label="类型" path="type">
<n-radio-group v-model:value="formParams.type" name="type">
<n-radio-button
v-for="menuType in menuTypes"
:key="menuType.value"
:value="menuType.value"
:label="menuType.label"
/>
</n-radio-group>
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="上级目录" path="pid">
<n-tree-select
filterable
:options="optionTreeDataEdit"
:value="formParams.pid"
@update:value="handleUpdateValue"
/>
</n-form-item>
</n-gi>
</n-grid>
<n-grid cols="2 300:1 600:2">
<n-gi>
<n-form-item
:label="
formParams.type === 1
? '目录名称'
: formParams.type === 2
? '菜单名称'
: '按钮名称'
"
path="title"
>
<n-input placeholder="请输入" v-model:value="formParams.title" />
</n-form-item>
</n-gi>
<n-gi v-if="formParams.type !== 3">
<n-form-item path="icon">
<IconSelector style="width: 100%" v-model:value="formParams.icon" option="antd" />
<template #label>
<n-tooltip trigger="hover">
<template #trigger>
<n-icon :component="QuestionCircleOutlined" :size="18" :depth="3" />
</template>
请填写图标编码可以参考图标库也可以不填使用默认图标
</n-tooltip>
菜单图标
</template>
</n-form-item>
</n-gi>
</n-grid>
<n-grid cols="2 300:1 600:2">
<n-gi v-if="formParams.type !== 3">
<n-form-item path="path">
<n-input placeholder="路由地址" v-model:value="formParams.path" />
<template #label>
<n-tooltip trigger="hover">
<template #trigger>
<n-icon :component="QuestionCircleOutlined" :size="18" :depth="3" />
</template>
请路由地址user
</n-tooltip>
路由地址
</template>
</n-form-item>
</n-gi>
<n-gi>
<n-form-item path="name">
<n-input placeholder="路由别名" v-model:value="formParams.name" />
<template #label>
<n-tooltip trigger="hover">
<template #trigger>
<n-icon :component="QuestionCircleOutlined" :size="18" :depth="3" />
</template>
对应路由配置文件中 `name` 只能是唯一性配置 `http(s)://` 开头地址
则会新窗口打开
</n-tooltip>
路由别名
</template>
</n-form-item>
</n-gi>
</n-grid>
<n-grid cols="2 300:1 600:2" v-if="formParams.type !== 3">
<n-gi>
<n-form-item label="组件路径" path="component">
<n-input placeholder="组件路径" v-model:value="formParams.component" />
<template #feedback>
主目录填 `LAYOUT`;多级父目录填
`ParentLayout`;页面填具体的组件路径`/system/menu/menu`
</template>
</n-form-item>
</n-gi>
<n-gi v-if="formParams.type === 1">
<n-form-item label="默认跳转" path="redirect">
<n-input placeholder="默认路由跳转地址" v-model:value="formParams.redirect" />
<template #feedback
>默认跳转路由地址`/system/menu/menu` 多级路由情况下适用
</template>
</n-form-item>
</n-gi>
</n-grid>
<n-divider title-placement="left">功能设置</n-divider>
<n-grid cols="1 ">
<n-gi>
<n-form-item label="分配权限" path="permissions">
<n-input
:type="formParams.permissions.length > 30 ? 'textarea' : ''"
placeholder="请输入分配权限,多个权限用,分割"
v-model:value="formParams.permissions"
/>
<template #label>
<n-tooltip trigger="hover">
<template #trigger>
<n-icon :component="QuestionCircleOutlined" :size="18" :depth="3" />
</template>
请填写API路由地址可同时作用于服务端和web端多个权限用,分割
</n-tooltip>
分配权限
</template>
</n-form-item>
</n-gi>
<!-- <n-gi>-->
<!-- <n-form-item label="权限名称" path="permissionName">-->
<!-- <n-input placeholder="权限名称" v-model:value="formParams.permissionName" />-->
<!-- <template #feedback>分配权限存在多个时权限名称只绑定到第一个权限</template>-->
<!-- </n-form-item>-->
<!-- </n-gi>-->
</n-grid>
<n-grid cols="2 300:1 600:2">
<n-gi v-if="formParams.type !== 3">
<n-form-item label="高亮路由" path="activeMenu">
<n-input placeholder="高亮路由" v-model:value="formParams.activeMenu" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="菜单排序" path="sort">
<n-input-number style="width: 100%" v-model:value="formParams.sort" clearable />
</n-form-item>
</n-gi>
</n-grid>
<n-grid cols="4 300:1 400:2 600:3 800:4" v-if="formParams.type !== 3">
<n-gi>
<n-form-item label="根路由" path="isRoot">
<n-radio-group v-model:value="formParams.isRoot" name="isRoot">
<n-radio-button
v-for="switchStatus in statusMap"
:key="switchStatus.value"
:value="switchStatus.value"
:label="switchStatus.label"
/>
</n-radio-group>
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="页签固定" path="affix">
<n-radio-group v-model:value="formParams.affix" name="affix">
<n-radio-button
v-for="switchStatus in statusMap"
:key="switchStatus.value"
:value="switchStatus.value"
:label="switchStatus.label"
/>
</n-radio-group>
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="简化路由" path="alwaysShow">
<n-radio-group v-model:value="formParams.alwaysShow" name="alwaysShow">
<n-radio-button
v-for="switchStatus in statusMap"
:key="switchStatus.value"
:value="switchStatus.value"
:label="switchStatus.label"
/>
</n-radio-group>
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="缓存路由" path="keepAlive">
<n-radio-group v-model:value="formParams.keepAlive" name="keepAlive">
<n-radio-button
v-for="switchStatus in statusMap"
:key="switchStatus.value"
:value="switchStatus.value"
:label="switchStatus.label"
/>
</n-radio-group>
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="是否隐藏" path="hidden">
<n-radio-group v-model:value="formParams.hidden" name="hidden">
<n-radio-button
v-for="hidden in hiddenMap"
:key="hidden.value"
:value="hidden.value"
:label="hidden.label"
/>
</n-radio-group>
</n-form-item>
</n-gi>
</n-grid>
<n-grid cols="4 300:1 400:2 600:3 800:4">
<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 statusMap"
:key="status.value"
:value="status.value"
:label="status.label"
/>
</n-radio-group>
</n-form-item>
</n-gi>
<n-gi v-if="formParams.type !== 3">
<n-form-item label="是否外链" path="isFrame">
<n-radio-group v-model:value="formParams.isFrame" name="isFrame">
<n-radio-button
v-for="switchStatus in statusMap"
:key="switchStatus.value"
:value="switchStatus.value"
:label="switchStatus.label"
/>
</n-radio-group>
</n-form-item>
</n-gi>
<n-gi v-if="formParams.type !== 3">
<n-form-item label="外部地址" path="frameSrc" v-show="formParams.isFrame === 1">
<n-input placeholder="内联外部地址" v-model:value="formParams.frameSrc" />
</n-form-item>
</n-gi>
</n-grid>
<n-form-item path="auth" style="margin-left: 100px">
<n-space>
<n-button type="primary" :loading="subLoading" @click="formSubmit"
>保存修改
</n-button>
<n-button @click="handleReset">重置</n-button>
<n-button @click="handleDel">删除</n-button>
</n-space>
</n-form-item>
</n-form>
<EditForm
v-if="formParams.id > 0"
v-model:formParams="formParams"
v-model:treeOption="treeOption"
@reloadTable="loadTreeOption"
/>
</n-card>
</n-gi>
</n-grid>
<CreateDrawer
ref="createDrawerRef"
:title="drawerTitle"
:optionTreeData="optionTreeData"
@loadData="loadData"
/>
<AddModal ref="addModalRef" v-model:treeOption="treeOption" @reloadTable="loadTreeOption" />
</div>
</template>
<script lang="ts" setup>
import { onMounted, reactive, ref, unref, computed } from 'vue';
import { FormItemRule, useDialog, useMessage } from 'naive-ui';
import {
AlignLeftOutlined,
FormOutlined,
PlusOutlined,
QuestionCircleOutlined,
SearchOutlined,
} from '@vicons/antd';
import { DeleteMenu, EditMenu, getMenuList } from '@/api/system/menu';
import { getTreeItem } from '@/utils';
import CreateDrawer from './CreateDrawer.vue';
import IconSelector from '@/components/IconSelector/index.vue';
import { newState, State } from '@/views/permission/menu/model';
import { cloneDeep } from 'lodash-es';
import { computed, onMounted, ref, unref } from 'vue';
import { AlignLeftOutlined, FormOutlined, PlusOutlined, SearchOutlined } from '@vicons/antd';
import { getMenuList } from '@/api/system/menu';
import { newState, State, loadOptions } from '@/views/permission/menu/model';
import EditForm from '@/views/permission/menu/editForm.vue';
import AddModal from '@/views/permission/menu/addModal.vue';
const menuTypes = [
{
value: 1,
label: '目录',
},
{
value: 2,
label: '菜单',
},
{
value: 3,
label: '按钮',
},
].map((s) => {
return s;
});
const statusMap = [
{
value: 0,
label: '禁用',
},
{
value: 1,
label: '启用',
},
].map((s) => {
return s;
});
const hiddenMap = [
{
value: 0,
label: '否',
},
{
value: 1,
label: '是',
},
].map((s) => {
return s;
});
const rules = {
title: {
required: true,
message: '请输入名称',
trigger: 'blur',
},
label: {
required: true,
message: '请输入标题',
trigger: 'blur',
},
path: {
required: false,
message: '请输入路由地址',
trigger: 'blur',
validator: function (_rule: FormItemRule, value: any, callback: Function) {
if (formParams.type != 3 && !value) {
callback(new Error('请输入路由地址'));
}
},
},
};
const formRef: any = ref(null);
const createDrawerRef = ref();
const message = useMessage();
const dialog = useDialog();
const treeData = ref([]);
const loading = ref(true);
const subLoading = ref(false);
const isEditMenu = ref(false);
const treeItemTitle = ref('');
const addModalRef = ref();
const loading = ref(false);
const treeOption = ref([]);
const pattern = ref('');
const drawerTitle = ref('');
const treeItemKey = ref([]);
const expandedKeys = ref([]);
const optionTreeData = ref<any>([]);
const formParams = reactive<State>(newState(null));
const optionTreeDataEdit = computed(() => {
let list = cloneDeep(optionTreeData.value);
for (let i = 0; i < list.length; i++) {
const item = list[i];
if (item.id === formParams.id) {
item.disabled = true;
setChildrenDisabled(item.children);
} else {
const foundChild = findItemById(item.children, formParams.id);
if (foundChild) {
foundChild.disabled = true;
setChildrenDisabled(foundChild.children);
}
}
const formParams = ref<State>(newState(null));
const treeItemTitle = computed(() => {
if (formParams.value.id > 0) {
return formParams.value.label + ' #' + formParams.value.id;
}
return list;
return '';
});
function setChildrenDisabled(children) {
if (!children) return;
for (let i = 0; i < children.length; i++) {
const child = children[i];
child.disabled = true;
setChildrenDisabled(child.children);
}
}
function findItemById(children, id) {
if (!children) return null;
for (let i = 0; i < children.length; i++) {
const item = children[i];
if (item.id === id) {
return item;
}
const foundChild = findItemById(item.children, id);
if (foundChild) {
return foundChild;
}
}
return null;
}
const isModal = defineModel<boolean>('isModal', { default: false });
function openCreateDrawer() {
drawerTitle.value = '添加菜单';
const { openDrawer } = createDrawerRef.value;
openDrawer(0);
addModalRef.value.openModal(null);
}
function openChildCreateDrawer() {
drawerTitle.value = '添加菜单';
const { openDrawer } = createDrawerRef.value;
openDrawer(formParams.id);
const state = newState(null);
state.pid = formParams.value.id;
state.type = formParams.value.type;
addModalRef.value.openModal(state);
}
function selectedTree(keys) {
function selectedTree(keys: number[], option: any[]) {
let item = null;
if (keys.length) {
const treeItem = getTreeItem(unref(treeData), keys[0]);
treeItemKey.value = keys;
treeItemTitle.value = treeItem.label + ' #' + treeItem.id;
Object.assign(formParams, treeItem);
isEditMenu.value = true;
} else {
isEditMenu.value = false;
treeItemKey.value = [];
treeItemTitle.value = '';
item = option[0];
}
}
function handleDel() {
dialog.warning({
title: '提示',
content: `您确定要删除此菜单吗?`,
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
DeleteMenu({ ...formParams }).then(async (_res) => {
message.success('操作成功');
await loadData();
});
},
onNegativeClick: () => {
message.error('已取消');
},
});
}
function handleReset() {
const treeItem = getTreeItem(unref(treeData), treeItemKey.value[0]);
Object.assign(formParams, treeItem);
}
function formSubmit() {
formRef.value.validate((errors: boolean) => {
if (!errors) {
subLoading.value = true;
EditMenu({ ...formParams })
.then(async (_res) => {
subLoading.value = false;
message.success('操作成功');
await loadData();
})
.catch((_e: Error) => {
subLoading.value = false;
});
} else {
message.error('请填写完整信息');
}
});
formParams.value = newState(item);
}
function packHandle() {
if (expandedKeys.value.length) {
expandedKeys.value = [];
} else {
expandedKeys.value = unref(treeData).map((item: any) => item.key as string) as [];
expandedKeys.value = unref(treeOption).map((item: any) => item.key as string) as [];
}
}
// 处理选项更新
function handleUpdateValue(value) {
formParams.pid = value as number;
}
onMounted(async () => {
await loadData();
});
async function loadData() {
const treeMenuList = await getMenuList();
const keys = treeMenuList.list.map((item) => item.key);
Object.assign(formParams, keys);
treeData.value = [];
treeData.value = treeMenuList.list;
optionTreeData.value = [
{
id: 0,
key: 0,
label: '根目录',
pid: 0,
title: '根目录',
type: 1,
},
];
optionTreeData.value = optionTreeData.value.concat(treeMenuList.list);
loading.value = false;
}
function onExpandedKeys(keys) {
expandedKeys.value = keys;
}
// 加载菜单选项树
function loadTreeOption() {
const needLoading = treeOption.value.length == 0;
if (needLoading) {
loading.value = true;
}
getMenuList()
.then((res) => {
if (res.list && res.list.length > 0) {
treeOption.value = res.list;
} else {
treeOption.value = [];
}
})
.finally(() => {
if (needLoading) {
loading.value = false;
}
});
}
onMounted(() => {
loadTreeOption();
loadOptions();
});
</script>

View File

@@ -0,0 +1,43 @@
<template>
<div>
<n-modal
title="菜单管理"
v-model:show="showModal"
:block-scroll="false"
:mask-closable="false"
:show-icon="false"
:on-after-leave="handleCancel"
preset="card"
:style="{
width: dialogWidth,
}"
>
<Menu v-model:isModal="isModal" />
</n-modal>
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import Menu from './menu.vue';
import { adaModalWidth } from '@/utils/hotgo';
const showModal = ref(false);
const isModal = ref(true);
const dialogWidth = computed(() => {
return adaModalWidth(1280);
});
const emit = defineEmits(['reloadTable']);
function openModal() {
showModal.value = true;
}
function handleCancel() {
emit('reloadTable');
}
defineExpose({
openModal,
});
</script>

View File

@@ -1,8 +1,11 @@
import { ref } from 'vue';
import { cloneDeep } from 'lodash-es';
import { Option } from '@/utils/hotgo';
import { Dicts } from '@/api/dict/dict';
export interface State {
id: number;
pid: number;
pid?: number;
title: string;
name: string;
path: string;
@@ -12,7 +15,7 @@ export interface State {
redirect: string;
permissions: string;
permissionName: string;
component: string;
component?: string;
alwaysShow: number;
activeMenu: string;
isRoot: number;
@@ -23,9 +26,11 @@ export interface State {
affix: number;
status: number;
sort: number;
disabled: boolean;
children?: State[];
}
export const defaultState = {
export const defaultState: State = {
id: 0,
pid: 0,
title: '',
@@ -37,22 +42,70 @@ export const defaultState = {
redirect: '',
permissions: '',
permissionName: '',
component: '',
alwaysShow: 1,
component: null,
alwaysShow: 2,
activeMenu: '',
isRoot: 0,
isRoot: 2,
isFrame: 2,
frameSrc: '',
keepAlive: 0,
hidden: 0,
affix: 0,
keepAlive: 2,
hidden: 2,
affix: 2,
status: 1,
sort: 10,
disabled: false,
children: null,
};
export function newState(state: State | null): State {
if (state !== null) {
return cloneDeep(state);
return defaultValueCheck(cloneDeep(state));
}
return cloneDeep(defaultState);
return defaultValueCheck(cloneDeep(defaultState));
}
// 默认值校正,主要为了解决历史数据格式不规范问题
export function defaultValueCheck(state: State): State {
if (state.pid < 1) {
state.pid = null;
}
if (state.alwaysShow != 1) {
state.alwaysShow = 2;
}
if (state.isRoot != 1) {
state.isRoot = 2;
}
if (state.isFrame != 1) {
state.isFrame = 2;
}
if (state.keepAlive != 1) {
state.keepAlive = 2;
}
if (state.hidden != 1) {
state.hidden = 2;
}
if (state.affix != 1) {
state.affix = 2;
}
if (state.status != 1) {
state.status = 2;
}
return state;
}
// 字典数据选项
export const options = ref({
sys_menu_types: [] as Option[],
sys_menu_component: [] as Option[],
sys_normal_disable: [] as Option[],
sys_switch: [] as Option[],
});
// 加载字典数据选项
export function loadOptions() {
Dicts({
types: ['sys_menu_types', 'sys_menu_component', 'sys_normal_disable', 'sys_switch'],
}).then((res) => {
options.value = res;
});
}

View File

@@ -46,27 +46,27 @@ export const columns = [
// title: '上级角色',
// key: 'pid',
// },
{
title: '默认角色',
key: 'isDefault',
render(row) {
return h(
NTag,
{
type: row.id == 1 ? 'success' : 'error',
},
{
default: () => (row.id == 1 ? '是' : '否'),
}
);
},
width: 80,
},
{
title: '排序',
key: 'sort',
width: 100,
},
// {
// title: '默认角色',
// key: 'isDefault',
// render(row) {
// return h(
// NTag,
// {
// type: row.id == 1 ? 'success' : 'error',
// },
// {
// default: () => (row.id == 1 ? '是' : '否'),
// }
// );
// },
// width: 80,
// },
// {
// title: '排序',
// key: 'sort',
// width: 100,
// },
{
title: '备注',
key: 'remark',

View File

@@ -0,0 +1,116 @@
<template>
<div>
<n-modal
v-model:show="showModal"
:mask-closable="false"
:show-icon="false"
preset="dialog"
transform-origin="center"
:title="'修改 ' + formValue.name + ' 的数据权限'"
>
<n-scrollbar style="max-height: 87vh" class="pr-5">
<n-spin :show="loading" description="请稍候...">
<n-form
:model="formValue"
ref="formRef"
:label-placement="settingStore.isMobile ? 'top' : 'left'"
:label-width="100"
class="py-4"
>
<n-form-item label="数据范围" path="dataScope">
<n-select v-model:value="formValue.dataScope" :options="dataScopeOption" />
</n-form-item>
<n-form-item label="自定义权限" path="customDept" v-if="formValue.dataScope === 4">
<n-tree-select
multiple
key-field="id"
:options="deptList"
v-model:value="formValue.customDept"
:default-expand-all="true"
/>
</n-form-item>
</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 } from 'vue';
import { DataScopeEdit, DataScopeSelect } from '@/api/system/role';
import { useProjectSettingStore } from '@/store/modules/projectSetting';
import { useMessage } from 'naive-ui';
import { newState, State } from '@/views/permission/role/model';
import { getDeptList } from '@/api/org/dept';
const emit = defineEmits(['reloadTable']);
const message = useMessage();
const settingStore = useProjectSettingStore();
const loading = ref(false);
const showModal = ref(false);
const formValue = ref<State>(newState(null));
const formRef = ref();
const formBtnLoading = ref(false);
const dataScopeOption = ref<any>([]);
const deptList = ref<any>([]);
function confirmForm(e) {
e.preventDefault();
formBtnLoading.value = true;
formRef.value.validate((errors) => {
if (!errors) {
DataScopeEdit(formValue.value).then((_res) => {
message.success('操作成功');
setTimeout(() => {
showModal.value = false;
emit('reloadTable');
});
});
} else {
message.error('请填写完整信息');
}
formBtnLoading.value = false;
});
}
function closeForm() {
showModal.value = false;
loading.value = false;
}
async function loadDataScopeSelect() {
const res = await DataScopeSelect();
if (res.list) {
dataScopeOption.value = res.list;
}
}
async function loadDeptList() {
const res = await getDeptList({});
if (res.list) {
deptList.value = res.list;
}
}
async function openModal(record: Recordable) {
showModal.value = true;
loading.value = true;
formValue.value = newState(record);
await loadDeptList();
await loadDataScopeSelect();
loading.value = false;
}
defineExpose({
openModal,
});
</script>
<style lang="less"></style>

View File

@@ -0,0 +1,216 @@
<template>
<div>
<n-modal
v-model:show="showModal"
:show-icon="false"
:mask-closable="false"
preset="dialog"
:title="'分配 ' + formValue.name + ' 的菜单权限'"
>
<n-spin :show="loading" description="请稍候...">
<div class="py-3 menu-list" :style="{ maxHeight: '90vh', height: '70vh' }">
<n-input size="small" v-model:value="pattern" placeholder="输入菜单名称搜索" class="mb-2">
<template #suffix>
<n-icon size="18" class="cursor-pointer">
<SearchOutlined />
</n-icon>
</template>
</n-input>
<n-tree
block-line
checkable
check-on-click
default-expand-all
virtual-scroll
:data="treeData"
:pattern="pattern"
:expandedKeys="expandedKeys"
:checked-keys="checkedKeys"
style="max-height: 950px; overflow: hidden"
@update:checked-keys="checkedTree"
@update:expanded-keys="onExpandedKeys"
/>
</div>
</n-spin>
<template #action>
<n-space class="mt-6" v-if="showImportSelect">
<n-input-group>
<n-tree-select
size="small"
placeholder="请选择一个要导入的角色"
:consistent-menu-width="false"
clearable
filterable
default-expand-all
:options="editRoleOption"
key-field="id"
label-field="name"
:on-update:value="handleImportSelect"
/>
<div class="mr-2"></div>
<n-button ghost @click="showImportSelect = false" size="small"> 取消 </n-button>
</n-input-group>
</n-space>
<n-space class="mt-6 space-group" v-if="!showImportSelect" size="small">
<n-button ghost @click="showImportSelect = true" size="small"> 导入权限 </n-button>
<n-button type="info" ghost icon-placement="left" @click="packHandle" size="small">
全部{{ expandedKeys.length ? '收起' : '展开' }}
</n-button>
<n-button type="info" ghost icon-placement="left" @click="checkedAllHandle" size="small">
全部{{ checkedAll ? '取消' : '选择' }}
</n-button>
<n-popconfirm @positive-click="confirmForm">
<template #trigger>
<n-button type="primary" :loading="formBtnLoading" size="small">提交</n-button>
</template>
你正在修改 {{ formValue.name }} 的菜单权限确定要提交吗
</n-popconfirm>
</n-space>
</template>
</n-modal>
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { GetPermissions, getRoleList, UpdatePermissions } from '@/api/system/role';
import { useProjectSettingStore } from '@/store/modules/projectSetting';
import { NButton, useMessage } from 'naive-ui';
import { adaModalWidth, getTreeKeys } from '@/utils/hotgo';
import { findTreeNode, getAllExpandKeys } from '@/utils';
import { getMenuList } from '@/api/system/menu';
import { SearchOutlined } from '@vicons/antd';
import { State, newState } from '@/views/permission/role/model';
const emit = defineEmits(['reloadTable']);
const message = useMessage();
const settingStore = useProjectSettingStore();
const loading = ref(false);
const showModal = ref(false);
const formValue = ref<State>(newState(null));
const formBtnLoading = ref(false);
const rawRoleOption = ref<State[]>([]);
const checkedAll = ref(false);
const treeData = ref([]);
const expandedKeys = ref<any[]>([]);
const checkedKeys = ref<any[]>([]);
const pattern = ref('');
const showImportSelect = ref(false);
const editRoleOption = computed<State[]>(() => {
if (!rawRoleOption.value) {
return [];
}
const role = findTreeNode(rawRoleOption.value, formValue.value.id, 'id');
if (role) {
role.disabled = true;
}
return rawRoleOption.value;
});
const dialogWidth = computed(() => {
return adaModalWidth(840);
});
function confirmForm(e) {
e.preventDefault();
formBtnLoading.value = true;
const params = {
id: formValue.value.id,
menuIds: checkedKeys.value ?? [],
};
UpdatePermissions(params)
.then((_res) => {
message.success('操作成功');
setTimeout(() => {
showModal.value = false;
emit('reloadTable');
});
})
.finally(() => {
formBtnLoading.value = false;
});
}
function closeForm() {
showModal.value = false;
loading.value = false;
}
function checkedTree(keys) {
checkedKeys.value = keys;
}
function onExpandedKeys(keys) {
expandedKeys.value = keys;
}
function packHandle() {
if (expandedKeys.value.length) {
expandedKeys.value = [];
} else {
expandedKeys.value = getAllExpandKeys(treeData) as [];
}
}
function checkedAllHandle() {
if (!checkedAll.value) {
checkedKeys.value = getTreeKeys(treeData.value);
checkedAll.value = true;
} else {
checkedKeys.value = [];
checkedAll.value = false;
}
}
function handleImportSelect(key: number) {
showImportSelect.value = false;
showModal.value = true;
getPermissions(key);
// 默认全部展开
expandedKeys.value = getAllExpandKeys(treeData);
message.success('导入成功,提交后生效');
}
async function loadMenuList() {
const res = await getMenuList();
expandedKeys.value = getAllExpandKeys(res.list) as [];
treeData.value = res.list;
}
async function getPermissions(id: number) {
checkedKeys.value = [];
checkedAll.value = false;
const res = await GetPermissions({ id: id });
checkedKeys.value = res.menuIds;
}
async function loadDataList() {
const res = await getRoleList({ pageSize: 100, page: 1 });
rawRoleOption.value = res.list;
}
async function openModal(record: Recordable) {
loading.value = true;
formValue.value = newState(record);
showModal.value = true;
await loadMenuList();
await getPermissions(record.id);
await loadDataList();
loading.value = false;
}
defineExpose({
openModal,
});
</script>
<style lang="less">
.space-group {
margin-left: -8px;
margin-right: -8px;
}
</style>

View File

@@ -0,0 +1,146 @@
<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 : '添加角色'"
>
<n-scrollbar style="max-height: 87vh" class="pr-5">
<n-spin :show="loading" description="请稍候...">
<n-form
:model="formValue"
:rules="rules"
ref="formRef"
:label-placement="settingStore.isMobile ? 'top' : 'left'"
:label-width="100"
class="py-4"
>
<n-form-item label="上级角色" path="pid">
<n-tree-select
:options="editRoleOption"
v-model:value="formValue.pid"
key-field="id"
label-field="name"
clearable
filterable
default-expand-all
/>
</n-form-item>
<n-form-item label="角色名称" path="name">
<n-input placeholder="请输入名称" v-model:value="formValue.name" />
</n-form-item>
<n-form-item label="角色编码" path="key">
<n-input placeholder="请输入" v-model:value="formValue.key" />
</n-form-item>
<n-form-item label="排序" path="sort">
<n-input-number v-model:value="formValue.sort" clearable style="width: 100%" />
</n-form-item>
<n-form-item label="状态" path="status">
<n-radio-group v-model:value="formValue.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="formValue.remark" />
</n-form-item>
</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 { computed, ref } from 'vue';
import { Edit, getRoleList } from '@/api/system/role';
import { useProjectSettingStore } from '@/store/modules/projectSetting';
import { useMessage } from 'naive-ui';
import { statusOptions } from '@/enums/optionsiEnum';
import { newState, State } from '@/views/permission/role/model';
const emit = defineEmits(['reloadTable']);
const message = useMessage();
const settingStore = useProjectSettingStore();
const loading = ref(false);
const showModal = ref(false);
const formValue = ref<State>(newState(null));
const formRef = ref();
const formBtnLoading = ref(false);
const rawRoleOption = ref([]);
const editRoleOption = computed(() => {
return rawRoleOption.value;
});
const rules = {
name: {
required: true,
trigger: ['blur', 'input'],
message: '请输入名称',
},
key: {
required: true,
trigger: ['blur', 'input'],
message: '请输入角色编码',
},
};
function confirmForm(e) {
e.preventDefault();
formBtnLoading.value = true;
formRef.value.validate((errors) => {
if (!errors) {
Edit(formValue.value).then((_res) => {
message.success('操作成功');
setTimeout(() => {
showModal.value = false;
emit('reloadTable');
});
});
} else {
message.error('请填写完整信息');
}
formBtnLoading.value = false;
});
}
function closeForm() {
showModal.value = false;
loading.value = false;
}
function loadDataList() {
loading.value = true;
getRoleList({ pageSize: 100, page: 1 }).then((res) => {
rawRoleOption.value = res.list;
loading.value = false;
});
}
function openModal(record: Recordable) {
loadDataList();
showModal.value = true;
formValue.value = newState(record);
}
defineExpose({
openModal,
});
</script>
<style lang="less"></style>

View File

@@ -0,0 +1,40 @@
import { cloneDeep } from 'lodash-es';
export class State {
id = 0;
name = '';
key = '';
dataScope = 1;
customDept: any[] = [];
pid = 0;
level = 1;
tree = '';
remark = '';
sort = 0;
status = 1;
createdAt = '';
updatedAt = '';
label = '';
value = 0;
children: State[] | null = null;
constructor(state?: Partial<State>) {
if (state) {
Object.keys(this).forEach((key) => {
if (state[key] !== undefined) {
this[key] = state[key];
}
});
}
}
}
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();
}

View File

@@ -14,9 +14,9 @@
</n-space>
<n-data-table
v-if="data.length > 0 || !loading"
v-if="dataSource.length > 0 || !loading"
:columns="columns.concat(actionColumn)"
:data="data"
:data="dataSource"
:row-key="(row) => row.id"
:loading="loading"
:resizeHeightOffset="-20000"
@@ -25,135 +25,9 @@
</n-space>
</n-card>
<n-modal
v-model:show="showModal"
:show-icon="false"
:mask-closable="false"
preset="dialog"
:title="editRoleTitle"
>
<div class="py-3 menu-list" :style="{ maxHeight: '90vh', height: '70vh' }">
<n-tree
block-line
checkable
:check-on-click="true"
:default-expand-all="true"
:virtual-scroll="true"
:data="treeData"
:expandedKeys="expandedKeys"
:checked-keys="checkedKeys"
style="max-height: 950px; overflow: hidden"
@update:checked-keys="checkedTree"
@update:expanded-keys="onExpandedKeys"
/>
</div>
<template #action>
<n-space>
<n-button type="info" ghost icon-placement="left" @click="packHandle">
全部{{ expandedKeys.length ? '收起' : '展开' }}
</n-button>
<n-button type="info" ghost icon-placement="left" @click="checkedAllHandle">
全部{{ checkedAll ? '取消' : '选择' }}
</n-button>
<n-button type="primary" :loading="formBtnLoading" @click="confirmForm">提交</n-button>
</n-space>
</template>
</n-modal>
<n-modal
v-model:show="showModal2"
:show-icon="false"
preset="dialog"
:title="formParams.id > 0 ? '编辑角色 #' + formParams.id : '添加角色'"
>
<n-form
:model="formParams"
:rules="rules"
ref="formRef"
label-placement="left"
:label-width="80"
class="py-4"
>
<n-form-item label="上级角色" path="pid">
<n-tree-select
:options="optionTreeData"
:default-value="formParams.pid"
key-field="id"
label-field="name"
:on-update:value="onUpdateValuePid"
/>
</n-form-item>
<n-form-item label="角色名称" path="name">
<n-input placeholder="请输入名称" v-model:value="formParams.name" />
</n-form-item>
<n-form-item label="权限编码" path="key">
<n-input placeholder="请输入" v-model:value="formParams.key" />
</n-form-item>
<!-- <n-form-item label="排序" path="sort">-->
<!-- <n-input-number v-model:value="formParams.sort" clearable />-->
<!-- </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="() => (showModal2 = false)">取消</n-button>
<n-button type="info" :loading="formBtnLoading2" @click="confirmForm2">确定</n-button>
</n-space>
</template>
</n-modal>
<n-modal
v-model:show="showDataModal"
:show-icon="false"
preset="dialog"
:title="'修改 ' + dataForm?.name + ' 的数据权限'"
>
<n-form
:model="dataForm"
ref="dataFormRef"
label-placement="left"
:label-width="120"
class="py-4"
>
<n-form-item label="数据范围" path="dataScope">
<n-select v-model:value="dataForm.dataScope" :options="dataScopeOption" />
</n-form-item>
<n-form-item label="自定义权限" path="customDept" v-if="dataForm.dataScope === 4">
<n-tree-select
multiple
key-field="id"
:options="deptList"
v-model:value="dataForm.customDept"
:default-expand-all="true"
@update:value="handleUpdateDeptValue"
/>
</n-form-item>
</n-form>
<template #action>
<n-space>
<n-button @click="() => (showDataModal = false)">取消</n-button>
<n-button type="info" :loading="dataFormBtnLoading" @click="confirmDataForm"
>确定</n-button
>
</n-space>
</template>
</n-modal>
<EditRole ref="editRoleRef" @reloadTable="reloadTable" />
<EditMenuAuth ref="editMenuAuthRef" @reloadTable="reloadTable" />
<EditDataAuth ref="editDataAuthRef" @reloadTable="reloadTable" />
</div>
</template>
@@ -161,77 +35,24 @@
import { h, onMounted, reactive, ref } from 'vue';
import { NButton, useDialog, useMessage } from 'naive-ui';
import { BasicColumn, TableAction } from '@/components/Table';
import {
Delete,
Edit,
GetPermissions,
getRoleList,
UpdatePermissions,
DataScopeSelect,
DataScopeEdit,
} from '@/api/system/role';
import { EditMenu, getMenuList } from '@/api/system/menu';
import { Delete, getRoleList } from '@/api/system/role';
import { columns } from './columns';
import { PlusOutlined } from '@vicons/antd';
import { getAllExpandKeys, getTreeAll } from '@/utils';
import { statusOptions } from '@/enums/optionsiEnum';
import { cloneDeep } from 'lodash-es';
import { getDeptList } from '@/api/org/dept';
import EditRole from './editRole.vue';
import EditMenuAuth from './editMenuAuth.vue';
import EditDataAuth from './editDataAuth.vue';
import { newState } from '@/views/permission/role/model';
const formRef: any = ref(null);
const message = useMessage();
const dialog = useDialog();
const showModal2 = ref(false);
const showModal = ref(false);
const formBtnLoading = ref(false);
const formBtnLoading2 = ref(false);
const checkedAll = ref<any>(false);
const editRoleTitle = ref('');
const treeData = ref([]);
const expandedKeys = ref([]);
const checkedKeys = ref<any>([]);
const updatePermissionsParams = ref<any>({});
const optionTreeData = ref<any>([]);
const dataScopeOption = ref<any>();
const deptList = ref<any>([]);
const dataFormRef = ref<any>();
const dataFormBtnLoading = ref(false);
const showDataModal = ref(false);
const dataForm = ref<any>();
const loading = ref(false);
const data = ref<any>([]);
const rules = {
name: {
required: true,
trigger: ['blur', 'input'],
message: '请输入名称',
},
key: {
required: true,
trigger: ['blur', 'input'],
message: '请输入角色编码',
},
};
const defaultState = {
id: 0,
pid: 0,
level: 1,
tree: '',
name: '',
key: '',
remark: null,
status: 1,
sort: 0,
dataScope: 1,
customDept: [],
};
const formParams = ref<any>(cloneDeep(defaultState));
const dataSource = ref<any>([]);
const editRoleRef = ref();
const editMenuAuthRef = ref();
const editDataAuthRef = ref();
const actionColumn = reactive<BasicColumn>({
width: 300,
width: 200,
title: '操作',
key: 'action',
fixed: 'right',
@@ -255,12 +76,13 @@
},
type: 'default',
},
{
label: '添加',
onClick: handleAddSub.bind(null, record),
},
{
label: '编辑',
onClick: handleEdit.bind(null, record),
ifShow: () => {
return record.id !== 1;
},
},
{
label: '删除',
@@ -274,62 +96,30 @@
},
});
const loadDataTable = async (res: any) => {
function loadDataTable() {
loading.value = true;
const tmp = await getRoleList({ ...res, ...{ pageSize: 100, page: 1 } });
data.value = tmp.list ?? [];
loading.value = false;
};
getRoleList({ pageSize: 100, page: 1 }).then((res) => {
dataSource.value = res.list ?? [];
loading.value = false;
});
}
function reloadTable() {
loadDataList();
loadDataTable({});
}
function confirmForm(e: any) {
e.preventDefault();
formBtnLoading.value = true;
UpdatePermissions({
...{
id: updatePermissionsParams.value.id,
menuIds:
checkedKeys.value === undefined || checkedKeys.value == null ? [] : checkedKeys.value,
},
}).then((_res) => {
message.success('操作成功');
reloadTable();
showModal.value = false;
formBtnLoading.value = false;
});
}
function confirmForm2(e) {
e.preventDefault();
formBtnLoading2.value = true;
formRef.value.validate((errors) => {
if (!errors) {
Edit(formParams.value).then((_res) => {
message.success('操作成功');
setTimeout(() => {
showModal2.value = false;
reloadTable();
});
});
} else {
message.error('请填写完整信息');
}
formBtnLoading2.value = false;
});
loadDataTable();
}
function addTable() {
showModal2.value = true;
formParams.value = cloneDeep(defaultState);
editRoleRef.value.openModal(null);
}
function handleEdit(record: Recordable) {
showModal2.value = true;
formParams.value = cloneDeep(record);
editRoleRef.value.openModal(record);
}
function handleAddSub(record: Recordable) {
let state = newState(null);
state.pid = record.id;
editRoleRef.value.openModal(state);
}
function handleDelete(record: Recordable) {
@@ -348,118 +138,16 @@
}
async function handleMenuAuth(record: Recordable) {
editRoleTitle.value = `分配 ${record.name} 的菜单权限`;
checkedKeys.value = [];
checkedAll.value = false;
const data = await GetPermissions({ ...{ id: record.id } });
checkedKeys.value = data.menuIds;
updatePermissionsParams.value.id = record.id;
showModal.value = true;
editMenuAuthRef.value.openModal(record);
}
function handleDataAuth(record: Recordable) {
dataForm.value = cloneDeep(record);
showDataModal.value = true;
editDataAuthRef.value.openModal(record);
}
function handleUpdateDeptValue(value: string | number | Array<string | number> | null) {
dataForm.value.customDept = value;
}
function confirmDataForm(e) {
e.preventDefault();
dataFormBtnLoading.value = true;
dataFormRef.value.validate((errors) => {
if (!errors) {
DataScopeEdit(dataForm.value).then((_res) => {
message.success('操作成功');
setTimeout(() => {
showDataModal.value = false;
reloadTable();
});
});
} else {
message.error('请填写完整信息');
}
dataFormBtnLoading.value = false;
});
}
function checkedTree(keys) {
checkedKeys.value = keys;
}
function onExpandedKeys(keys) {
expandedKeys.value = keys;
}
function packHandle() {
if (expandedKeys.value.length) {
expandedKeys.value = [];
} else {
expandedKeys.value = getAllExpandKeys(treeData) as [];
}
}
function checkedAllHandle() {
if (!checkedAll.value) {
checkedKeys.value = getTreeAll(treeData.value);
checkedAll.value = true;
} else {
checkedKeys.value = [];
checkedAll.value = false;
}
}
onMounted(async () => {
loadDataList();
loadMenuList();
loadDeptList();
loadDataScopeSelect();
await loadDataTable({});
onMounted(() => {
loadDataTable();
});
function loadDataList() {
getRoleList({ pageSize: 100, page: 1 }).then((res) => {
optionTreeData.value = [
{
id: 0,
key: 0,
label: '顶级角色',
pid: 0,
name: '顶级角色',
},
];
optionTreeData.value = optionTreeData.value.concat(res.list);
});
}
function loadMenuList() {
getMenuList().then((res) => {
expandedKeys.value = getAllExpandKeys(res.list) as [];
treeData.value = res.list;
});
}
function loadDeptList() {
getDeptList({}).then((res) => {
if (res.list) {
deptList.value = res.list;
}
});
}
function loadDataScopeSelect() {
DataScopeSelect().then((res) => {
if (res.list) {
dataScopeOption.value = res.list;
}
});
}
function onUpdateValuePid(value: string | number) {
formParams.value.pid = value;
}
</script>
<style lang="less" scoped></style>

View File

@@ -68,7 +68,7 @@
/>
</n-form-item>
<n-form-item label="默认注册岗位" path="loginPostIds">
<n-form-item label="默认注册岗位(非必选)" path="loginPostIds">
<n-select v-model:value="formValue.loginPostIds" multiple :options="options.post" />
</n-form-item>
@@ -182,22 +182,17 @@
function load() {
show.value = true;
new Promise((_resolve, _reject) => {
getConfig({ group: group.value })
.then((res) => {
show.value = false;
formValue.value = res.list;
})
.catch((error) => {
show.value = false;
message.error(error.toString());
});
});
getConfig({ group: group.value })
.then((res) => {
formValue.value = res.list;
})
.finally(() => {
show.value = false;
});
}
onMounted(async () => {
show.value = true;
await loadOptions();
load();
await loadOptions();
});
</script>

View File

@@ -12,18 +12,22 @@ export const columns = [
{
title: 'ID',
key: 'id',
width: 80,
},
{
title: '任务标题',
key: 'title',
width: 150,
},
{
title: '任务分组',
key: 'groupName',
width: 100,
},
{
title: '执行方法',
key: 'name',
width: 100,
},
// {
// title: '执行参数',
@@ -50,10 +54,12 @@ export const columns = [
}
);
},
width: 100,
},
{
title: '表达式',
key: 'pattern',
width: 150,
},
// {
// title: '执行次数',
@@ -77,9 +83,11 @@ export const columns = [
}
);
},
width: 100,
},
{
title: '创建时间',
key: 'createdAt',
width: 180,
},
];

View File

@@ -21,7 +21,8 @@
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1090"
:scroll-x="scrollX"
:resizeHeightOffset="-10000"
>
<template #tableTitle>
<n-button type="primary" @click="addTable">
@@ -181,7 +182,7 @@
</template>
<script lang="ts" setup>
import { h, reactive, ref, onBeforeMount } from 'vue';
import { h, reactive, ref, onBeforeMount, computed } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
@@ -189,6 +190,7 @@
import { columns } from './columns';
import { DeleteOutlined, GroupOutlined, PlusOutlined } from '@vicons/antd';
import GroupModal from './modal/modal.vue';
import { adaTableScrollX } from '@/utils/hotgo';
const optionTreeData = ref<any>([]);
const defaultValueRef = () => ({
@@ -335,7 +337,7 @@
width: 320,
title: '操作',
key: 'action',
// fixed: 'right',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
@@ -370,6 +372,10 @@
},
});
const scrollX = computed(() => {
return adaTableScrollX(columns, actionColumn.width);
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,

View File

@@ -3,7 +3,7 @@ import { NTag } from 'naive-ui';
export const columns = [
{
title: 'id',
title: '字典ID',
key: 'id',
},
{

View File

@@ -126,7 +126,6 @@
SearchOutlined,
DeleteOutlined,
} from '@vicons/antd';
import { getTreeItem } from '@/utils';
import CreateDrawer from './CreateDrawer.vue';
import List from './list.vue';
import { DeleteDict, getDictTree } from '@/api/dict/dict';
@@ -143,16 +142,7 @@
const checkedId = ref(0);
const pattern = ref('');
const drawerTitle = ref('');
const optionTreeData = ref([
{
id: 0,
key: 0,
label: '根目录',
pid: 0,
title: '根目录',
type: 1,
},
]);
const optionTreeData = ref([]);
const defaultValueRef = () => ({
id: 0,
pid: 0,
@@ -177,10 +167,9 @@
openDrawer(formParams);
}
function selectedTree(keys) {
function selectedTree(keys, opts) {
if (keys.length) {
const treeItem = getTreeItem(unref(treeData), keys[0]);
// console.log('选择treeItem:' + JSON.stringify(treeItem));
const treeItem = opts[0];
treeItemKey.value = keys;
treeItemTitle.value = treeItem.label;
Object.assign(formParams, treeItem);
@@ -200,15 +189,10 @@
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
DeleteDict({ ...formParams })
.then(async (_res) => {
message.success('操作成功');
// handleReset();
await loadData();
})
.catch((_e: Error) => {
// message.error(e.message ?? '操作失败');
});
DeleteDict({ ...formParams }).then(async (_res) => {
message.success('操作成功');
await loadData();
});
},
onNegativeClick: () => {
message.error('已取消');

View File

@@ -97,7 +97,7 @@
<script lang="ts" setup>
import { h, reactive, ref, watch, onMounted } from 'vue';
import { TreeSelectOption, useMessage, useDialog, NTag, SelectRenderTag } from 'naive-ui';
import { useMessage, useDialog, NTag, SelectRenderTag } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
import { getDataList, getDictSelect, EditData, DeleteData } from '@/api/dict/dict';
@@ -106,9 +106,10 @@
import { statusOptions } from '@/enums/optionsiEnum';
import { TypeSelect } from '@/api/sys/config';
import { Option } from '@/utils/hotgo';
import { findTreeDataById } from '@/utils';
import { findTreeNode } from '@/utils';
import { cloneDeep } from 'lodash-es';
const options = ref<Option>();
import { defRangeShortcuts } from '@/utils/dateUtil';
interface Props {
checkedId?: number;
}
@@ -132,15 +133,28 @@
{
field: 'label',
component: 'NInput',
label: '标签',
label: '字典标签',
componentProps: {
placeholder: '请输入标签名称',
placeholder: '请输入字典标签',
onInput: (e: any) => {
console.log(e);
params.value.label = e;
},
},
rules: [{ message: '请输入字典标签名称', trigger: ['blur'] }],
rules: [{ message: '请输入字典标签', trigger: ['blur'] }],
},
{
field: 'value',
component: 'NInput',
label: '字典键值',
componentProps: {
placeholder: '请输入字典键值',
onInput: (e: any) => {
console.log(e);
params.value.value = e;
},
},
rules: [{ message: '请输入字典键值', trigger: ['blur'] }],
},
];
@@ -194,17 +208,19 @@
const showModal = ref(false);
const formBtnLoading = ref(false);
const formParams = ref<any>({ typeId: 0 });
const options = ref<Option>();
const params = ref({
pageSize: 10,
typeId: props.checkedId,
label: '',
value: '',
});
const actionColumn = reactive({
width: 220,
width: 120,
title: '操作',
key: 'action',
// fixed: 'right',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
@@ -223,7 +239,7 @@
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:1 l:2 xl:2 2xl:2' },
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
@@ -286,9 +302,6 @@
reloadTable();
});
},
onNegativeClick: () => {
// message.error('取消');
},
});
}
@@ -322,7 +335,7 @@
}
function handleUpdateTypeIdValue(value) {
const row = findTreeDataById(typeList.value, value);
const row = findTreeNode(typeList.value, value, 'id');
if (!row) {
message.error('未找到该节点数据');
return;

View File

@@ -0,0 +1,151 @@
<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:2 l:2 xl:2 2xl:2" responsive="screen">
<n-gi span="2">
<n-form-item label="分类名称" path="name">
<n-input placeholder="请输入分类名称" v-model:value="formValue.name" />
</n-form-item>
</n-gi>
<n-gi span="2">
<n-form-item label="简称" path="shortName">
<n-input placeholder="请输入简称" v-model:value="formValue.shortName" />
</n-form-item>
</n-gi>
<n-gi span="2">
<n-form-item label="描述" path="description">
<n-input type="textarea" placeholder="描述" v-model:value="formValue.description" />
</n-form-item>
</n-gi>
<n-gi span="1">
<n-form-item label="排序" path="sort">
<n-input-number placeholder="请输入排序" v-model:value="formValue.sort" />
</n-form-item>
</n-gi>
<n-gi span="1">
<n-form-item label="状态" path="status">
<n-select v-model:value="formValue.status" :options="options.sys_normal_disable" />
</n-form-item>
</n-gi>
<n-gi span="2">
<n-form-item label="备注" path="remark">
<n-input type="textarea" placeholder="备注" v-model:value="formValue.remark" />
</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/testCategory';
import { options, State, newState, rules } from './model';
import { useProjectSettingStore } from '@/store/modules/projectSetting';
import { useMessage } from 'naive-ui';
import { adaModalWidth } from '@/utils/hotgo';
const emit = defineEmits(['reloadTable']);
const message = useMessage();
const settingStore = useProjectSettingStore();
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(840);
});
function openModal(state: State) {
showModal.value = true;
// 新增
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>

View File

@@ -0,0 +1,190 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="测试分类">
<!-- 这是由系统生成的CURD表格你可以将此行注释改为表格的描述 -->
</n-card>
</div>
<n-card :bordered="false" class="proCard">
<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" :request="loadDataTable" :row-key="(row) => row.id" :actionColumn="actionColumn" :scroll-x="scrollX" :resizeHeightOffset="-10000" :checked-row-keys="checkedIds" @update:checked-row-keys="handleOnCheckedRow">
<template #tableTitle>
<n-button type="primary" @click="addTable" class="min-left-space" v-if="hasPermission(['/testCategory/edit'])">
<template #icon>
<n-icon>
<PlusOutlined />
</n-icon>
</template>
添加
</n-button>
<n-button type="error" @click="handleBatchDelete" class="min-left-space" v-if="hasPermission(['/testCategory/delete'])">
<template #icon>
<n-icon>
<DeleteOutlined />
</n-icon>
</template>
批量删除
</n-button>
</template>
</BasicTable>
</n-card>
<Edit ref="editRef" @reloadTable="reloadTable" />
</div>
</template>
<script lang="ts" setup>
import { h, reactive, ref, computed, 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 { List, Delete, Status } from '@/api/testCategory';
import { PlusOutlined, DeleteOutlined } from '@vicons/antd';
import { columns, schemas, options, loadOptions } from './model';
import { adaTableScrollX, getOptionLabel } 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 actionColumn = reactive({
width: 216,
title: '操作',
key: 'action',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
auth: ['/testCategory/edit'],
},
{
label: '禁用',
onClick: handleStatus.bind(null, record, 2),
ifShow: () => {
return record.status === 1;
},
auth: ['/testCategory/status'],
},
{
label: '启用',
onClick: handleStatus.bind(null, record, 1),
ifShow: () => {
return record.status === 2;
},
auth: ['/testCategory/status'],
},
{
label: '删除',
onClick: handleDelete.bind(null, record),
auth: ['/testCategory/delete'],
},
],
});
},
});
const scrollX = computed(() => {
return adaTableScrollX(columns, actionColumn.width);
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
// 加载表格数据
const loadDataTable = async (res) => {
return await List({ ...searchFormRef.value?.formModel, ...res });
};
// 更新选中的行
function handleOnCheckedRow(rowKeys) {
checkedIds.value = rowKeys;
}
// 重新加载表格数据
function reloadTable() {
actionRef.value?.reload();
}
// 添加数据
function addTable() {
editRef.value.openModal(null);
}
// 编辑数据
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 handleStatus(record: Recordable, status: number) {
Status({ id: record.id, status: status }).then((_res) => {
message.success('设为' + getOptionLabel(options.value.sys_normal_disable, status) + '成功');
setTimeout(() => {
reloadTable();
});
});
}
onMounted(() => {
loadOptions();
});
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,184 @@
import { h, ref } from 'vue';
import { NTag } from 'naive-ui';
import { cloneDeep } from 'lodash-es';
import { FormSchema } from '@/components/Form';
import { Dicts } from '@/api/dict/dict';
import { isNullObject } from '@/utils/is';
import { defRangeShortcuts } from '@/utils/dateUtil';
import { Option, getOptionLabel, getOptionTag } from '@/utils/hotgo';
export class State {
public id = 0; // 分类ID
public name = ''; // 分类名称
public shortName = ''; // 简称
public description = ''; // 描述
public sort = 0; // 排序
public status = 1; // 状态
public remark = ''; // 备注
public createdAt = ''; // 创建时间
public updatedAt = ''; // 修改时间
public deletedAt = ''; // 删除时间
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 = {
name: {
required: true,
trigger: ['blur', 'input'],
type: 'string',
message: '请输入分类名称',
},
sort: {
required: true,
trigger: ['blur', 'input'],
type: 'number',
message: '请输入排序',
},
};
// 表格搜索表单
export const schemas = ref<FormSchema[]>([
{
field: 'id',
component: 'NInputNumber',
label: '分类ID',
componentProps: {
placeholder: '请输入分类ID',
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'name',
component: 'NInput',
label: '分类名称',
componentProps: {
placeholder: '请输入分类名称',
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'status',
component: 'NSelect',
label: '状态',
defaultValue: null,
componentProps: {
placeholder: '请选择状态',
options: [],
onUpdateValue: (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 columns = [
{
title: '分类ID',
key: 'id',
align: 'left',
width: 80,
},
{
title: '分类名称',
key: 'name',
align: 'left',
width: -1,
},
{
title: '简称',
key: 'shortName',
align: 'left',
width: 80,
},
{
title: '描述',
key: 'description',
align: 'left',
width: 300,
},
{
title: '状态',
key: 'status',
align: 'left',
width: -1,
render(row) {
if (isNullObject(row.status)) {
return ``;
}
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: getOptionTag(options.value.sys_normal_disable, row.status),
bordered: false,
},
{
default: () => getOptionLabel(options.value.sys_normal_disable, row.status),
}
);
},
},
{
title: '创建时间',
key: 'createdAt',
align: 'left',
width: 180,
},
];
// 字典数据选项
export const options = ref({
sys_normal_disable: [] as Option[],
});
// 加载字典数据选项
export function loadOptions() {
Dicts({
types: ['sys_normal_disable'],
}).then((res) => {
options.value = res;
for (const item of schemas.value) {
switch (item.field) {
case 'status':
item.componentProps.options = options.value.sys_normal_disable;
break;
}
}
});
}