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

@@ -1,6 +1,6 @@
{
"name": "hotgo",
"version": "2.13.1",
"version": "2.15.1",
"author": {
"name": "MengShuai",
"email": "133814250@qq.com",
@@ -30,18 +30,18 @@
"dependencies": {
"@vicons/antd": "^0.12.0",
"@vicons/ionicons5": "^0.12.0",
"@vue/runtime-core": "^3.4.19",
"@vue/runtime-core": "^3.4.21",
"@vueup/vue-quill": "^1.2.0",
"@vueuse/core": "^10.7.1",
"axios": "^0.21.4",
"@vueuse/core": "^10.9.0",
"axios": "^1.6.8",
"date-fns": "^2.28.0",
"echarts": "^5.3.2",
"echarts": "^5.5.0",
"element-resize-detector": "^1.2.4",
"fingerprintjs2": "^2.1.4",
"highlight.js": "^11.8.0",
"lodash-es": "^4.17.21",
"naive-ui": "^2.36.0",
"mint-filter": "^4.0.3",
"naive-ui": "^2.38.1",
"pinia": "^2.1.7",
"pinyin-pro": "^3.16.3",
"print-js": "^1.6.0",
@@ -49,14 +49,14 @@
"qs": "^6.10.3",
"quill-image-uploader": "^1.3.0",
"quill-magic-url": "^4.2.0",
"spark-md5": "^3.0.2",
"throttle-debounce": "^5.0.0",
"vue": "^3.4.0",
"vue-router": "^4.2.5",
"vue": "^3.4.21",
"vue-router": "^4.3.0",
"vue-types": "^5.1.1",
"vue-waterfall-plugin-next": "^2.2.3",
"vue3-json-viewer": "^2.2.2",
"vuedraggable": "^4.1.0",
"vue-waterfall-plugin-next": "^2.2.3",
"spark-md5": "^3.0.2",
"weixin-js-sdk": "^1.6.0"
},
"devDependencies": {
@@ -88,7 +88,7 @@
"less": "^4.1.2",
"less-loader": "^9.1.0",
"lint-staged": "^11.2.6",
"postcss": "^8.4.13",
"postcss": "^8.4.38",
"prettier": "^2.6.2",
"pretty-quick": "^3.1.3",
"rimraf": "^3.0.2",
@@ -98,9 +98,9 @@
"stylelint-order": "^4.1.0",
"stylelint-scss": "^3.21.0",
"tailwindcss": "^2.2.19",
"typescript": "^4.6.4",
"typescript": "^5.3.0",
"unplugin-vue-components": "^0.17.21",
"vite": "^4.2.7",
"vite": "^4.5.3",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-html": "^2.1.2",
"vite-plugin-require-transform": "^1.0.5",

View File

@@ -43,6 +43,10 @@
primaryColor: appTheme,
primaryColorHover: lightenStr,
primaryColorPressed: lightenStr,
// 纵向滚动条宽
scrollbarWidth: '10px',
// 横向滚动条高
scrollbarHeight: '10px',
},
LoadingBar: {
colorLoading: appTheme,

View File

@@ -0,0 +1,42 @@
import { http, jumpExport } from '@/utils/http/axios';
// 获取多租户功能演示列表
export function List(params) {
return http.request({
url: '/hgexample/tenantOrder/list',
method: 'get',
params,
});
}
// 删除/批量删除多租户功能演示
export function Delete(params) {
return http.request({
url: '/hgexample/tenantOrder/delete',
method: 'POST',
params,
});
}
// 添加/编辑多租户功能演示
export function Edit(params) {
return http.request({
url: '/hgexample/tenantOrder/edit',
method: 'POST',
params,
});
}
// 获取多租户功能演示指定详情
export function View(params) {
return http.request({
url: '/hgexample/tenantOrder/view',
method: 'GET',
params,
});
}
// 导出多租户功能演示
export function Export(params) {
jumpExport('/hgexample/tenantOrder/export', params);
}

View File

@@ -1,6 +1,6 @@
import { http, jumpExport } from '@/utils/http/axios';
// 获取生成演示列表
// 获取CURD列表列表
export function List(params) {
return http.request({
url: '/curdDemo/list',
@@ -9,7 +9,7 @@ export function List(params) {
});
}
// 删除/批量删除生成演示
// 删除/批量删除CURD列表
export function Delete(params) {
return http.request({
url: '/curdDemo/delete',
@@ -18,8 +18,7 @@ export function Delete(params) {
});
}
// 添加/编辑生成演示
// 添加/编辑CURD列表
export function Edit(params) {
return http.request({
url: '/curdDemo/edit',
@@ -28,18 +27,7 @@ export function Edit(params) {
});
}
// 修改生成演示状态
export function Status(params) {
return http.request({
url: '/curdDemo/status',
method: 'POST',
params,
});
}
// 操作生成演示开关
// 操作CURD列表开关
export function Switch(params) {
return http.request({
url: '/curdDemo/switch',
@@ -48,8 +36,7 @@ export function Switch(params) {
});
}
// 获取生成演示指定详情
// 获取CURD列表指定详情
export function View(params) {
return http.request({
url: '/curdDemo/view',
@@ -58,8 +45,7 @@ export function View(params) {
});
}
// 获取生成演示最大排序
// 获取CURD列表最大排序
export function MaxSort() {
return http.request({
url: '/curdDemo/maxSort',
@@ -67,8 +53,7 @@ export function MaxSort() {
});
}
// 导出生成演示
// 导出CURD列表
export function Export(params) {
jumpExport('/curdDemo/export', params);
}
}

View File

@@ -0,0 +1,53 @@
import { http, jumpExport } from '@/utils/http/axios';
// 获取普通树表列表
export function List(params) {
return http.request({
url: '/normalTreeDemo/list',
method: 'get',
params,
});
}
// 删除/批量删除普通树表
export function Delete(params) {
return http.request({
url: '/normalTreeDemo/delete',
method: 'POST',
params,
});
}
// 添加/编辑普通树表
export function Edit(params) {
return http.request({
url: '/normalTreeDemo/edit',
method: 'POST',
params,
});
}
// 获取普通树表指定详情
export function View(params) {
return http.request({
url: '/normalTreeDemo/view',
method: 'GET',
params,
});
}
// 获取普通树表最大排序
export function MaxSort() {
return http.request({
url: '/normalTreeDemo/maxSort',
method: 'GET',
});
}
// 获取普通树表关系树选项
export function TreeOption() {
return http.request({
url: '/normalTreeDemo/treeOption',
method: 'GET',
});
}

View File

@@ -0,0 +1,53 @@
import { http, jumpExport } from '@/utils/http/axios';
// 获取选项树表列表
export function List(params) {
return http.request({
url: '/optionTreeDemo/list',
method: 'get',
params,
});
}
// 删除/批量删除选项树表
export function Delete(params) {
return http.request({
url: '/optionTreeDemo/delete',
method: 'POST',
params,
});
}
// 添加/编辑选项树表
export function Edit(params) {
return http.request({
url: '/optionTreeDemo/edit',
method: 'POST',
params,
});
}
// 获取选项树表指定详情
export function View(params) {
return http.request({
url: '/optionTreeDemo/view',
method: 'GET',
params,
});
}
// 获取选项树表最大排序
export function MaxSort() {
return http.request({
url: '/optionTreeDemo/maxSort',
method: 'GET',
});
}
// 获取选项树表关系树选项
export function TreeOption() {
return http.request({
url: '/optionTreeDemo/treeOption',
method: 'GET',
});
}

View File

@@ -16,6 +16,7 @@ export function Edit(params) {
});
}
// 部门状态
export function Status(params) {
return http.request({
url: '/dept/status',
@@ -24,6 +25,23 @@ export function Status(params) {
});
}
// 获取管理员_部门指定详情
export function View(params) {
return http.request({
url: '/dept/view',
method: 'GET',
params,
});
}
// 获取管理员_部门最大排序
export function MaxSort() {
return http.request({
url: '/dept/maxSort',
method: 'GET',
});
}
export function Delete(params) {
return http.request({
url: '/dept/delete',
@@ -40,3 +58,11 @@ export function getDeptOption() {
params,
});
}
// 部门关系树选项
export function TreeOption() {
return http.request({
url: '/dept/treeOption',
method: 'GET',
});
}

View File

@@ -20,14 +20,6 @@ export function Edit(params) {
});
}
export function Status(params) {
return http.request({
url: '/post/status',
method: 'POST',
params,
});
}
export function Delete(params) {
return http.request({
url: '/post/delete',

View File

@@ -0,0 +1,54 @@
import { http, jumpExport } from '@/utils/http/axios';
// 获取测试分类列表
export function List(params) {
return http.request({
url: '/testCategory/list',
method: 'get',
params,
});
}
// 删除/批量删除测试分类
export function Delete(params) {
return http.request({
url: '/testCategory/delete',
method: 'POST',
params,
});
}
// 添加/编辑测试分类
export function Edit(params) {
return http.request({
url: '/testCategory/edit',
method: 'POST',
params,
});
}
// 修改测试分类状态
export function Status(params) {
return http.request({
url: '/testCategory/status',
method: 'POST',
params,
});
}
// 获取测试分类指定详情
export function View(params) {
return http.request({
url: '/testCategory/view',
method: 'GET',
params,
});
}
// 获取测试分类最大排序
export function MaxSort() {
return http.request({
url: '/testCategory/maxSort',
method: 'GET',
});
}

View File

@@ -1,93 +1,99 @@
<template>
<n-date-picker v-bind="$props" v-model:value="modelValue" :shortcuts="shortcuts" :clearable="true"/>
<n-date-picker
v-bind="$props"
v-model:value="modelValue"
:shortcuts="showShortcuts ? shortcuts : undefined"
:clearable="true"
style="width: 100%"
/>
</template>
<script lang="ts">
import { computed, defineComponent, onMounted, ref } from 'vue';
import {
dateToTimestamp,
formatToDate,
formatToDateTime,
timestampToTime,
defShortcuts,
defRangeShortcuts,
} from '@/utils/dateUtil';
import { basicProps } from './props';
import { computed, defineComponent, onMounted, ref } from 'vue';
import {
dateToTimestamp,
defRangeShortcuts,
defShortcuts,
formatToDate,
formatToDateTime,
timestampToTime,
} from '@/utils/dateUtil';
import { basicProps } from './props';
export default defineComponent({
name: 'BasicUpload',
props: {
...basicProps,
},
emits: ['update:formValue', 'update:startValue', 'update:endValue'],
setup(props, { emit }) {
const shortcuts = ref<any>({});
export default defineComponent({
name: 'DatePicker',
props: {
...basicProps,
},
emits: ['update:formValue', 'update:startValue', 'update:endValue'],
setup(props, { emit }) {
const shortcuts = ref<any>({});
function getTimestamp(value) {
let t = dateToTimestamp(value);
if (t === 0) {
return new Date().getTime();
function getTimestamp(value) {
let t = dateToTimestamp(value);
console.log('getTimestamp t:' + t);
if (t === 0) {
return undefined;
}
return t;
}
return t;
}
function setTimestamp(value) {
if (!isTimeType()) {
return formatToDate(new Date(Number(value)).toDateString());
} else {
return formatToDateTime(timestampToTime(Number(value / 1000)));
}
}
function isRangeType() {
return props.type.indexOf('range') != -1;
}
function isTimeType() {
return props.type.indexOf('time') != -1;
}
const modelValue = computed({
get() {
if (!isRangeType()) {
const value = getTimestamp(props.formValue);
if (props.formValue == ""){
emit('update:formValue', setTimestamp(value));
}
return value;
function setTimestamp(value) {
if (value === undefined) {
return undefined;
}
if (!isTimeType()) {
return formatToDate(new Date(Number(value)).toDateString());
} else {
const value = [getTimestamp(props.startValue), getTimestamp(props.endValue)];
if (props.startValue == "" && props.endValue == ""){
return formatToDateTime(timestampToTime(Number(value / 1000)));
}
}
function isRangeType() {
return props.type.indexOf('range') != -1;
}
function isTimeType() {
return props.type.indexOf('time') != -1;
}
const modelValue = computed({
get() {
if (!isRangeType()) {
return getTimestamp(props.formValue);
} else {
const value = [getTimestamp(props.startValue), getTimestamp(props.endValue)];
if (!value[0] && !value[1]) {
return null;
}
return value;
}
},
set(value) {
if (!isRangeType()) {
emit('update:formValue', setTimestamp(value));
} else {
emit('update:startValue', setTimestamp(value[0]));
emit('update:endValue', setTimestamp(value[1]));
}
return value
}
},
set(value) {
},
});
onMounted(async () => {
if (!isRangeType()) {
emit('update:formValue', setTimestamp(value));
shortcuts.value = defShortcuts();
} else {
emit('update:startValue', setTimestamp(value[0]));
emit('update:endValue', setTimestamp(value[1]));
shortcuts.value = defRangeShortcuts();
}
},
});
});
onMounted(async () => {
if (!isRangeType()) {
shortcuts.value = defShortcuts();
} else {
shortcuts.value = defRangeShortcuts();
}
});
return {
modelValue,
shortcuts,
};
},
});
return {
modelValue,
shortcuts,
showShortcuts: props.showShortcuts,
};
},
});
</script>
<style lang="less"></style>

View File

@@ -15,4 +15,8 @@ export const basicProps = {
type: String as PropType<string> | undefined | Date,
default: () => '',
},
showShortcuts: {
type: Boolean as PropType<boolean>,
default: () => true,
},
};

View File

@@ -126,12 +126,14 @@
const emit = defineEmits(['update:value']);
const fileUploadRef = ref();
const dialogWidth = ref('85%');
const dialog = useDialog();
const showModal = ref(false);
const chooserRef = ref();
const previewRef = ref();
const fileList = ref<string[]>([]);
const dialogWidth = computed(() => {
return adaModalWidth(1080);
});
const getCSSProperties = computed(() => {
return {
@@ -246,7 +248,6 @@
);
onMounted(async () => {
adaModalWidth(dialogWidth, 1080);
loadImage();
});
</script>

View File

@@ -59,6 +59,13 @@
v-bind="getComponentProps(schema)"
/>
</template>
<template v-else-if="schema.component === 'NCascader'">
<n-cascader
:class="{ isFull: schema.isFull !== false && getProps.isFull }"
v-model:value="formModel[schema.field]"
v-bind="getComponentProps(schema)"
/>
</template>
<!--动态渲染表单组件-->
<component
v-else
@@ -127,7 +134,16 @@
</template>
<script lang="ts">
import { defineComponent, reactive, ref, computed, unref, onMounted, watch } from 'vue';
import {
defineComponent,
reactive,
ref,
computed,
unref,
onMounted,
watch,
defineExpose,
} from 'vue';
import { createPlaceholderMessage } from './helper';
import { useFormEvents } from './hooks/useFormEvents';
import { useFormValues } from './hooks/useFormValues';
@@ -139,8 +155,10 @@
import type { GridProps } from 'naive-ui/lib/grid';
import type { FormSchema, FormProps, FormActionType } from './types/form';
import { isArray } from '@/utils/is';
import {isArray, isBoolean, isFunction} from '@/utils/is';
import { deepMerge } from '@/utils';
import { usePermission } from '@/hooks/web/usePermission';
import {ActionItem} from "@/components/Table";
export default defineComponent({
name: 'BasicForm',
@@ -158,6 +176,7 @@
const gridCollapsed = ref(true);
const loadingSub = ref(false);
const isUpdateDefaultRef = ref(false);
const { hasPermission } = usePermission();
const getSubmitBtnOptions = computed(() => {
return Object.assign(
@@ -222,7 +241,12 @@
);
const getSchema = computed((): FormSchema[] => {
const schemas: FormSchema[] = unref(schemaRef) || (unref(getProps).schemas as any);
const rawSchemas: FormSchema[] = unref(schemaRef) || (unref(getProps).schemas as any);
const schemas = rawSchemas.filter((schema) => {
return hasPermission(schema.auth as string[]) && isIfShow(schema);
});
for (const schema of schemas) {
const { defaultValue } = schema;
// handle date type
@@ -240,6 +264,20 @@
formModel,
});
function isIfShow(action: ActionItem): boolean {
const ifShow = action.ifShow;
let isIfShow = true;
if (isBoolean(ifShow)) {
isIfShow = ifShow;
}
if (isFunction(ifShow)) {
isIfShow = ifShow(action);
}
return isIfShow;
}
const { handleSubmit, validate, resetFields, getFieldsValue, clearValidate, setFieldsValue } =
useFormEvents({
emit,
@@ -314,6 +352,7 @@
isInline,
getComponentProps,
unfoldToggle,
setFieldsValue,
};
},
});

View File

@@ -2,6 +2,8 @@ import { ComponentType } from './index';
import type { CSSProperties } from 'vue';
import type { GridProps, GridItemProps } from 'naive-ui/lib/grid';
import type { ButtonProps } from 'naive-ui/lib/button';
import {PermissionsEnum} from "@/enums/permissionsEnum";
import { ActionItem } from '@/components/Table';
export interface FormSchema {
field: string;
@@ -16,6 +18,8 @@ export interface FormSchema {
giProps?: GridItemProps;
isFull?: boolean;
suffix?: string;
auth?: PermissionsEnum | PermissionsEnum[] | string | string[];
ifShow?: boolean | ((action: ActionItem) => boolean);
}
export interface FormProps {

View File

@@ -7,12 +7,7 @@
<template v-else>
<AntdSelector v-model:value="formValue" />
</template>
<n-input
v-bind="$props"
:value="formValue"
:style="{ width: '70%' }"
placeholder="请选择图标"
/>
<n-input v-bind="$props" :value="formValue" placeholder="请选择图标" />
</n-input-group>
</div>
</template>

View File

@@ -94,6 +94,7 @@
import { useLockscreenStore } from '@/store/modules/lockscreen';
import { useUserStore } from '@/store/modules/user';
import { aesEcb } from '@/utils/encrypt';
import { TABS_ROUTES } from '@/store/mutation-types';
export default defineComponent({
name: 'Lockscreen',
@@ -149,6 +150,7 @@
if (code === ResultEnum.SUCCESS) {
onLockLogin(false);
useLockscreen.setLock(false);
window.location.reload();
} else {
state.errorMsg = message;
state.isLoginError = true;
@@ -160,11 +162,18 @@
const goLogin = () => {
onLockLogin(false);
useLockscreen.setLock(false);
router.replace({
path: '/login',
query: {
redirect: route.fullPath,
},
userStore.logout().then(() => {
// 移除标签页
localStorage.removeItem(TABS_ROUTES);
router
.replace({
name: 'Login',
query: {
redirect: route.fullPath,
},
})
.finally(() => location.reload());
});
};

View File

@@ -4,7 +4,6 @@
* 接收参数string类型/Ref<string>类型/Reactive<string>类型
*/
import type { Directive, DirectiveBinding } from 'vue';
import { useMessage } from 'naive-ui';
interface ElType extends HTMLElement {
copyData: string | number;
__handleClick__: any;

14
web/src/enums/deptEnum.ts Normal file
View File

@@ -0,0 +1,14 @@
// 部门类型
export enum DeptTypeEnum {
// 公司
Company = 'company',
// 租户
Tenant = 'tenant',
// 商户
Merchant = 'merchant',
// 用户
User = 'user',
}

View File

@@ -1,35 +1,3 @@
import { Option } from '@/utils/hotgo';
export const switchOptions = [
{
value: 1,
label: '已开启',
},
{
value: 2,
label: '已关闭',
},
].map((s) => {
return s;
});
export const sexOptions = [
{
value: 1,
label: '男',
},
{
value: 2,
label: '女',
},
{
value: 3,
label: '未知',
},
].map((s) => {
return s;
});
export const statusOptions = [
{
value: 1,
@@ -39,22 +7,7 @@ export const statusOptions = [
value: 2,
label: '停用',
},
].map((s) => {
return s;
});
export const hiddenOptions = [
{
value: 1,
label: '是',
},
{
value: 2,
label: '否',
},
].map((s) => {
return s;
});
];
// 操作类
export const statusActions = [
@@ -67,47 +20,3 @@ export const statusActions = [
key: 2,
},
];
// 标签
export const tagOptions = [
{
label: '灰色',
value: 'default',
},
{
label: '主色',
value: 'primary',
},
{
label: '蓝色',
value: 'info',
},
{
label: '绿色',
value: 'success',
},
{
label: '橙色',
value: 'warning',
},
{
label: '红色',
value: 'error',
},
];
// 登录状态
export const loginStatusOptions: Option[] = [
{
value: 1,
label: '成功',
key: 1,
listClass: 'success',
},
{
value: 2,
label: '失败',
key: 2,
listClass: 'warning',
},
];

View File

@@ -1,7 +0,0 @@
export enum RoleEnum {
// 管理员
ADMIN = 'admin',
// 普通用户
NORMAL = 'normal',
}

View File

@@ -7,7 +7,7 @@
>
<div class="logo" v-if="navMode === 'horizontal'">
<img src="~@/assets/images/logo.png" alt="" />
<h2 v-show="!collapsed" class="title">HotGo</h2>
<h2 v-show="!collapsed" class="title">{{ projectName }}</h2>
</div>
<AsideMenu
@update:collapsed="updateMenu"
@@ -135,7 +135,7 @@
</div>
<!-- 个人中心 -->
<div class="layout-header-trigger layout-header-trigger-min">
<n-dropdown trigger="hover" @select="avatarSelect" :options="avatarOptions">
<n-dropdown trigger="click" @select="avatarSelect" :options="avatarOptions" show-arrow>
<div class="avatar">
<n-avatar v-if="userStore.avatar" round :size="30" :src="userStore.avatar" />
<n-avatar v-else round :size="30">{{ userStore.realName }}</n-avatar>
@@ -187,6 +187,7 @@
useNotification,
NotificationReactive,
NButton,
NText,
} from 'naive-ui';
import { TABS_ROUTES } from '@/store/mutation-types';
import { useUserStore } from '@/store/modules/user';
@@ -233,6 +234,7 @@
// const { username, avatar } = userStore?.info || {};
const drawerSetting = ref();
const projectName = import.meta.env.VITE_GLOB_APP_TITLE;
const state = reactive({
// username: username || '',
@@ -373,7 +375,36 @@
},
},
];
function renderCustomHeader() {
return h(
'div',
{
style: 'display: flex; align-items: center; padding: 8px 12px;',
},
[
h('div', null, [
h('div', null, [
h(NText, { depth: 2 }, { default: () => userStore?.info?.username }),
]),
h('div', { style: 'font-size: 12px;' }, [
h(NText, { depth: 3 }, { default: () => userStore?.info?.roleName }),
]),
]),
]
);
}
const avatarOptions = [
{
key: 'header',
type: 'render',
render: renderCustomHeader,
},
{
type: 'divider',
key: 'd1',
},
{
label: '个人设置',
key: 1,
@@ -507,6 +538,7 @@
getIsMobile,
userStore,
updateMenu,
projectName,
};
},
});
@@ -537,6 +569,7 @@
overflow: hidden;
white-space: nowrap;
padding-left: 10px;
min-width: 200px;
img {
width: auto;
@@ -546,6 +579,7 @@
.title {
margin-bottom: 0;
min-width: 132px;
}
}

View File

@@ -172,10 +172,9 @@
const isMixMenuNoneSub = computed(() => {
const mixMenu = settingStore.menuSetting.mixMenu;
const currentRoute = useRoute();
const navMode = unref(getNavMode);
if (unref(navMode) != 'horizontal-mix') return true;
return !(unref(navMode) === 'horizontal-mix' && mixMenu && currentRoute.meta.isRoot);
return !(unref(navMode) === 'horizontal-mix' && mixMenu && route.meta.isRoot);
});
//动态组装样式 菜单缩进

View File

@@ -87,6 +87,7 @@
getMultiTabsSetting,
} = useProjectSetting();
const route = useRoute();
const settingStore = useProjectSettingStore();
const navMode = getNavMode;
@@ -105,11 +106,12 @@
return fixed ? 'absolute' : 'static';
});
const isMixMenuNoneSub = computed(() => {
const mixMenu = settingStore.menuSetting.mixMenu;
const currentRoute = useRoute();
// const currentRoute = useRoute();
if (unref(navMode) != 'horizontal-mix') return true;
if (unref(navMode) === 'horizontal-mix' && mixMenu && currentRoute.meta.isRoot) {
if (unref(navMode) === 'horizontal-mix' && mixMenu && route.meta.isRoot) {
return false;
}
return true;

View File

@@ -18,11 +18,13 @@ import {
mobileLogin,
} from '@/api/system/user';
import { isWechatBrowser } from '@/utils/is';
import { DeptTypeEnum } from '@/enums/deptEnum';
const Storage = createStorage({ storage: localStorage });
export interface UserInfoState {
id: number;
deptName: string;
deptType: string;
roleName: string;
cityLabel: string;
permissions: string[];
@@ -55,7 +57,7 @@ export interface ConfigState {
domain: string;
version: string;
wsAddr: string;
mode:string;
mode: string;
}
export interface LoginConfigState {
@@ -114,6 +116,18 @@ export const useUserStore = defineStore({
getLoginConfig(): LoginConfigState | null {
return this.loginConfig;
},
isCompanyDept(): boolean {
return this.info?.deptType == DeptTypeEnum.Company;
},
isTenantDept(): boolean {
return this.info?.deptType == DeptTypeEnum.Tenant;
},
isMerchantDept(): boolean {
return this.info?.deptType == DeptTypeEnum.Merchant;
},
isUserDept(): boolean {
return this.info?.deptType == DeptTypeEnum.User;
},
},
actions: {
setToken(token: string) {

View File

@@ -1,7 +1,8 @@
import { Ref, UnwrapRef } from '@vue/reactivity';
import onerrorImg from '@/assets/images/onerror.png';
import { NTag, SelectRenderTag } from 'naive-ui';
import { h } from 'vue';
import { usePermission } from '@/hooks/web/usePermission';
import { ActionItem } from '@/components/Table';
import { isBoolean, isFunction } from '@/utils/is';
import { PermissionsEnum } from '@/enums/permissionsEnum';
export interface Option {
label: string;
@@ -44,15 +45,50 @@ export function getOptionTag(options: Option[], value) {
}
// 自适应模板宽度
export function adaModalWidth(dialogWidth: Ref<UnwrapRef<string>>, def = 840) {
export function adaModalWidth(def = 840) {
const val = document.body.clientWidth;
if (val <= def) {
dialogWidth.value = '100%';
return '100%';
} else {
dialogWidth.value = def + 'px';
return def + 'px';
}
return dialogWidth.value;
}
interface TableColumn {
width?: number | string;
auth?: PermissionsEnum | PermissionsEnum[] | string | string[];
ifShow?: boolean | ((action: ActionItem) => boolean);
}
// 自适应表格组件横向滑动可见宽度
export function adaTableScrollX(columns: TableColumn[] = [], actionWidth: number) {
const { hasPermission } = usePermission();
let x = 50; // 勾选列宽度
columns = columns.filter((column) => {
return hasPermission(column.auth as string[]) && isIfShow(column);
});
for (const column of columns) {
if (column.width && Number(column.width) >= 1) {
x += Number(column.width);
} else {
x += 100; // 默认列宽度
}
}
x += actionWidth;
return x;
}
export function isIfShow(action: ActionItem): boolean {
let isIfShow = true;
const ifShow = action.ifShow;
if (isBoolean(ifShow)) {
isIfShow = ifShow;
}
if (isFunction(ifShow)) {
isIfShow = ifShow(action);
}
return isIfShow;
}
// 图片加载失败显示自定义默认图片(缺省图)
@@ -61,16 +97,6 @@ export function errorImg(e: any): void {
e.target.onerror = null;
}
export const renderTag: SelectRenderTag = ({ option }) => {
return h(
NTag,
{
type: option.listClass as 'success' | 'warning' | 'error' | 'info' | 'primary' | 'default',
},
{ default: () => option.label }
);
};
export function timeFix() {
const time = new Date();
const hour = time.getHours();
@@ -98,3 +124,32 @@ export function rdmLightRgbColor(): string {
}
return color;
}
// 将列表数据转为树形数据
export function convertListToTree(list: any[], idField = 'id', pidField = 'pid') {
const min = list.reduce((prev, current) => (prev[pidField] < current[pidField] ? prev : current));
const map = list.reduce((acc, item) => {
acc[item[idField]] = { ...item, children: [] };
return acc;
}, {});
list.forEach((item) => {
if (item[pidField] !== min[pidField]) {
map[item[pidField]].children.push(map[item[idField]]);
}
});
return list.filter((item) => item[pidField] === min[pidField]).map((item) => map[item[idField]]);
}
// 从树选项中获取所有key
export function getTreeKeys(data: any[], idField = 'id') {
const keys = [];
data.map((item) => {
keys.push(item[idField]);
if (item.children && item.children.length) {
keys.push(...getTreeKeys(item.children));
}
});
return keys;
}

View File

@@ -24,7 +24,7 @@ export function checkStatus(status: number, msg: string): void {
$message.error('网络请求超时');
break;
case 500:
$message.error('服务器错误,请联系管理员!');
$message.error('服务器错误,请稍候重试!');
break;
case 501:
$message.error('网络未实现');

View File

@@ -1,9 +1,21 @@
import { h, unref } from 'vue';
import type { App, Plugin } from 'vue';
import { NIcon, NTag, NTooltip } from 'naive-ui';
import {
NAvatar,
NBadge,
NButton,
NIcon,
NPopover,
NTable,
NTag,
NTooltip,
SelectRenderTag,
} from 'naive-ui';
import { EllipsisHorizontalCircleOutline } from '@vicons/ionicons5';
import { PageEnum } from '@/enums/pageEnum';
import { isObject } from './is/index';
import { cloneDeep } from 'lodash-es';
import { VNode } from '@vue/runtime-core';
export const renderTooltip = (trigger, content) => {
return h(NTooltip, null, {
@@ -38,6 +50,100 @@ export function renderNew(type = 'warning', text = 'New', color: object = newTag
);
}
// render 标记
export function renderBadge(node: VNode) {
return h(
NBadge,
{
dot: true,
type: 'info',
},
{ default: () => node }
);
}
// render 标签
export const renderTag: SelectRenderTag = ({ option }) => {
return h(
NTag,
{
type: option.listClass as 'success' | 'warning' | 'error' | 'info' | 'primary' | 'default',
},
{ default: () => option.label }
);
};
export interface MemberSumma {
id: number; // 用户ID
realName: string; // 真实姓名
username: string; // 用户名
avatar: string; // 头像
}
// render 操作人摘要
export const renderPopoverMemberSumma = (member?: MemberSumma) => {
if (!member) {
return '';
}
return h(
NPopover,
{ trigger: 'hover' },
{
trigger: () =>
h(
NButton,
{
strong: true,
size: 'small',
text: true,
iconPlacement: 'right',
},
{ default: () => member.realName, icon: renderIcon(EllipsisHorizontalCircleOutline) }
),
default: () =>
h(
NTable,
{
props: {
bordered: false,
'single-line': false,
size: 'small',
},
},
[
h('thead', [
h('tr', { align: 'center' }, [
h('th', '用户ID'),
h('th', '头像'),
h('th', '姓名'),
h('th', '用户名'),
]),
]),
h('tbody', [
h('tr', { align: 'center' }, [
h('td', member.id),
h('td', h(NAvatar, { src: member.avatar, round: true, size: 'small' })),
h('td', member.realName),
h('td', member.username),
]),
]),
]
),
}
);
};
// render html
export function renderHtmlTooltip(content: string) {
content = content.replace(/\n/g, '<br>');
const html = h('p', { id: 'app' }, [
h('div', {
innerHTML: content,
}),
]);
return renderTooltip(html, html);
}
/**
* 递归组装菜单格式
*/
@@ -174,35 +280,6 @@ export const withInstall = <T>(component: T, alias?: string) => {
return component as T & Plugin;
};
/**
* 找到对应的节点
* */
let result = null;
export function getTreeItem(data: any[], key?: string | number): any {
data.map((item) => {
if (item.key === key) {
result = item;
} else {
if (item.children && item.children.length) {
getTreeItem(item.children, key);
}
}
});
return result;
}
export function getTreeAll(data: any[]): any[] {
const treeAll: any[] = [];
data.map((item) => {
treeAll.push(item.key);
if (item.children && item.children.length) {
treeAll.push(...getTreeAll(item.children));
}
});
return treeAll;
}
// dynamic use hook props
export function getDynamicProps<T, U>(props: T): Partial<U> {
const ret: Recordable = {};
@@ -269,15 +346,18 @@ export function getAllExpandKeys(treeData: any): any[] {
return expandedKeys;
}
// 从树中查找指定ID
export function findTreeDataById(data: any[], id: number | string) {
// 从树中查找指定节点
export function findTreeNode(data: any[], key?: string | number, keyField = 'key'): any {
for (const item of data) {
if (item.id === id) {
if (item[keyField] == key) {
return item;
}
if (item.children) {
const found = findTreeDataById(item.children, id);
if (found) return found;
} else {
if (item.children && item.children.length) {
const foundItem = findTreeNode(item.children, key);
if (foundItem) {
return foundItem;
}
}
}
}
return null;

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 } });
};

Some files were not shown because too many files have changed in this diff Show More