Initial commit

This commit is contained in:
南宁网宿科技
2024-04-24 16:36:46 +08:00
commit 121348e011
991 changed files with 158700 additions and 0 deletions

View File

@@ -0,0 +1,233 @@
<!-- 用户编辑弹窗 -->
<template>
<ele-modal
width="400px"
:visible="visible"
:confirm-loading="loading"
:title="'手机验证'"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
:maskClosable="false"
@ok="save"
>
<a-form class="login-form">
<a-form-item label="绑定的手机号码" name="phone">
{{ getMobile(form.phone) }}
</a-form-item>
<a-form-item label="校验码" name="code">
<div class="login-input-group">
<a-input
allow-clear
type="text"
:maxlength="6"
v-model:value="form.code"
>
</a-input>
<a-button
class="login-captcha"
:disabled="!!countdownTime"
@click="openImgCodeModal"
>
<span v-if="!countdownTime">发送验证码</span>
<span v-else>已发送 {{ countdownTime }} s</span>
</a-button>
</div>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch, computed, onBeforeUnmount } from "vue";
import { Form, message, Modal, SelectProps } from "ant-design-vue";
import { useUserStore } from "@/store/modules/user";
import type { AccessKey } from "@/api/system/access-key/model";
import { addAccessKey, updateAccessKey } from "@/api/system/access-key";
import { FILE_SERVER } from "@/config/setting";
import { uploadFile } from "@/api/system/file";
import { RuleObject } from "ant-design-vue/es/form";
import { isImage } from "@/utils/common";
import { listUsers } from '@/api/system/user';
import { getMobile } from '@/utils/common';
import { sendSmsCaptcha } from '@/api/login';
const useForm = Form.useForm;
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: AccessKey | null;
}>();
const userStore = useUserStore();
// 当前登录用户信息
const loginUser = computed(() => userStore.info ?? {});
// 是否是修改
const isUpdate = ref(false);
const disabled = ref(false);
// 选项卡位置
const activeKey = ref("1");
const promoter = ref<any>(undefined);
const commander = ref(undefined);
const appid = ref(undefined);
/* 打开选择弹窗 */
const content = ref("");
// 图形验证码地址
const captcha = ref("");
// 验证码倒计时定时器
let countdownTimer: number | null = null;
// 验证码倒计时时间
const countdownTime = ref(0);
// 图形验证码
const imgCode = ref("");
// 发送验证码按钮loading
const codeLoading = ref(false);
const emit = defineEmits<{
(e: "done", form: AccessKey): void;
(e: "update:visible", value: boolean): void;
}>();
// 已上传数据, 可赋初始值用于回显
const avatar = ref(<any>[]);
// 提交状态
const loading = ref(false);
// 用户信息
const form = reactive<AccessKey>({
id: 0,
phone: "",
accessKey: "",
accessSecret: "",
code: undefined,
createTime: ""
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit("update:visible", value);
};
// 表单验证规则
const rules = reactive({
name: [
{
required: true,
type: "string",
message: "请输入工单名称",
trigger: "blur"
}
],
taskType: [
{
required: true,
type: "string",
message: "请选择工单类型",
trigger: "blur"
}
],
content: [
{
required: true,
type: "string",
message: "请输入工单内容",
trigger: "blur",
validator: async (_rule: RuleObject, value: string) => {
if (content.value == "") {
return Promise.reject("请输入文字内容");
}
return Promise.resolve();
}
}
]
});
/* 显示发送短信验证码弹窗 */
const openImgCodeModal = () => {
if (!form.phone) {
message.error("手机号码有误");
return;
}
// imgCode.value = "";
sendCode();
// visible.value = true;
};
/* 发送短信验证码 */
const sendCode = () => {
codeLoading.value = true;
sendSmsCaptcha({ phone: form.phone }).then((res) => {
console.log(res);
message.success("短信验证码发送成功, 请注意查收!");
codeLoading.value = false;
countdownTime.value = 30;
// 开始对按钮进行倒计时
countdownTimer = window.setInterval(() => {
if (countdownTime.value <= 1) {
countdownTimer && clearInterval(countdownTimer);
countdownTimer = null;
}
countdownTime.value--;
}, 1000);
});
};
onBeforeUnmount(() => {
countdownTimer && clearInterval(countdownTimer);
});
const { validate, validateInfos } = useForm(form, rules);
/* 保存编辑 */
const save = () => {
validate()
.then(() => {
updateVisible(false);
const { code,phone } = form;
emit("done", { code,phone });
})
.catch(() => {
});
};
const query = () => {
listUsers({username: 'admin'}).then(res => {
form.phone = res[0].phone;
})
}
query();
</script>
<style lang="less" scoped>
.login-form{
padding: 0 20px;
}
.login-form-right .login-form {
margin: 0 15% 0 auto;
}
.login-form-left .login-form {
margin: 0 auto 0 15%;
}
/* 验证码 */
.login-input-group {
display: flex;
align-items: center;
:deep(.ant-input-affix-wrapper) {
flex: 1;
}
.login-captcha {
margin-left: 10px;
padding: 0 10px;
& > img {
width: 100%;
height: 100%;
}
}
}
</style>

View File

@@ -0,0 +1,148 @@
<template>
<div class="page">
<a-page-header :ghost="false" title="秘钥管理">
<div class="ele-text-secondary">
AccessKey ID AccessKey Secret 是您访WebSoft-API
的密钥具有该账户完全的权限请您妥善保管
</div>
</a-page-header>
<div class="ele-body">
<a-card :bordered="false">
<!-- 表格 -->
<ele-pro-table
ref="tableRef"
row-key="logId"
:columns="columns"
:datasource="datasource"
:where="defaultWhere"
cache-key="userBalanceLogTable"
>
<template #toolbar>
<a-space>
<a-button type="primary" class="ele-btn-icon" @click="add">
<template #icon>
<plus-outlined />
</template>
<span>创建 AccessKey</span>
</a-button>
<a-button @click="reset">刷新</a-button>
</a-space>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'accessSecret'">
<span v-if="record.accessSecret">
{{ record.accessSecret }}
</span>
<a @click="openEdit(record)" v-else>查看 Secret</a>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">查看 Secret</a>
<!-- <a-divider type="vertical" />-->
<!-- <a @click="resetPsw(record)">禁用</a>-->
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
</div>
<!-- 编辑弹窗 -->
<AccessKeyEdit v-model:visible="showEdit" :data="current" @done="reload" />
</div>
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue';
import { message } from 'ant-design-vue/es';
import type { EleProTable } from 'ele-admin-pro/es';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import AccessKeyEdit from './components/accesskey-edit.vue';
import { toDateString } from 'ele-admin-pro/es';
import { addAccessKey, pageAccessKey } from '@/api/system/access-key';
import { AccessKey, AccessKeyParam } from '@/api/system/access-key/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: 'AccessKey ID',
key: 'accessKey',
dataIndex: 'accessKey',
showSorterTooltip: false
},
{
title: 'AccessSecret',
key: 'accessSecret',
dataIndex: 'accessSecret',
showSorterTooltip: false
},
{
title: '创建时间',
dataIndex: 'createTime',
customRender: ({ text }) => toDateString(text)
}
]);
// 表格选中数据
const selection = ref<AccessKey[]>([]);
const searchText = ref('');
const userId = ref<number>(0);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 当前编辑数据
const current = ref<any>(null);
// 默认搜索条件
const defaultWhere = reactive({
code: '',
phone: '',
username: '',
nickname: '',
userId: undefined
});
// 表格数据源
const datasource: DatasourceFunction = ({ page, limit, where, orders }) => {
return pageAccessKey({ ...where, ...orders, page, limit }).catch((e) => {
message.error(e.message);
});
};
const reset = () => {
userId.value = 0;
searchText.value = '';
reload();
};
/* 打开编辑弹窗 */
const openEdit = (row?: any) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 搜索 */
const reload = (where?: AccessKeyParam) => {
selection.value = [];
tableRef?.value?.reload({ page: 1, where });
};
const add = () => {
addAccessKey({})
.then((res) => {
reload();
})
.catch((err) => {
message.error(err.message);
});
};
</script>
<script lang="ts">
export default {
name: 'AccessKeyIndex'
};
</script>

View File

@@ -0,0 +1,225 @@
<!-- 用户编辑弹窗 -->
<template>
<ele-modal
:width="900"
:visible="visible"
v-if="data"
:confirm-loading="loading"
:title="`购买插件(${data.appName})`"
:maxable="true"
@update:visible="updateVisible"
@ok="save"
>
<a-tabs type="card" v-model:activeKey="activeKey" tabPosition="top">
<a-tab-pane key="1" tab="专业版">
<div class="pay-box">
<div class="qrcode">
<ele-qr-code
:value="text"
:size="200"
:margin="2"
:image-settings="image"
/>
</div>
<div class="pay-info">
<a-alert
message="提示:支付后请耐心等待支付结果,请勿刷新浏览器,否则将会导致购买异常。"
banner
closable
style="margin-bottom: 12px"
/>
<a-descriptions title="" :column="{ xs: 1, sm: 1, md: 1 }">
<a-descriptions-item label="模块名称">{{
data.appName
}}</a-descriptions-item>
<a-descriptions-item label="当前账号">{{
loginUser.nickname
}}</a-descriptions-item>
<a-descriptions-item label="订单金额"
><span class="ele-text-warning"
>¥{{ data.price }} /1</span
></a-descriptions-item
>
</a-descriptions>
</div>
</div>
</a-tab-pane>
<a-tab-pane key="2" tab="企业版">
<div class="pay-box">
<div class="qrcode">
<ele-qr-code
:value="text"
:size="200"
:margin="2"
:image-settings="image"
/>
</div>
<div class="pay-info">
<a-alert
message="提示:支付后请耐心等待支付结果,请勿刷新浏览器,否则将会导致购买异常。"
banner
closable
style="margin-bottom: 12px"
/>
<a-descriptions title="" :column="{ xs: 1, sm: 1, md: 1 }">
<a-descriptions-item label="模块名称">{{
data.appName
}}</a-descriptions-item>
<a-descriptions-item label="当前账号">{{
loginUser.nickname
}}</a-descriptions-item>
<a-descriptions-item label="订单金额"
><span class="ele-text-warning"
>¥{{ data.price }} /1</span
></a-descriptions-item
>
</a-descriptions>
</div>
</div>
</a-tab-pane>
<a-tab-pane key="3" tab="应用介绍">
<div class="app-info">
<!-- 编辑器 -->
<tinymce-editor
v-model:value="content"
:disabled="true"
:init="config"
/>
</div>
</a-tab-pane>
</a-tabs>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch, computed } from 'vue';
import { Form, message } from 'ant-design-vue';
import type { App } from '@/api/dashboard/appstore/model';
import type { ImageSettings } from 'ele-admin-pro/es/ele-qr-code/types';
import { useUserStore } from '@/store/modules/user';
import { installApp } from '@/api/system/menu';
import type { Menu } from '@/api/system/menu/model';
const useForm = Form.useForm;
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: App | null;
showView?: boolean;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 表单数据
const form = reactive<Menu>({
menuId: undefined,
parentId: undefined,
title: '',
menuType: 0,
openType: 0,
icon: '',
path: '',
component: '',
authority: '',
sortNumber: undefined,
hide: 0,
meta: '',
appId: undefined
});
const userStore = useUserStore();
// 当前用户信息
const loginUser = computed(() => userStore.info ?? {});
const content = ref('');
const activeKey = ref('1');
const loading = ref(false);
const text = ref('weixin://wxpay/bizpayurl?pr=r7C2C7Ozz');
const image = reactive<ImageSettings>({
src: 'https://cdn.eleadmin.com/20200610/logo-radius.png',
width: 28,
height: 28
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
const config = ref({
toolbar: false,
menubar: false,
height: 700
});
const { resetFields, validate } = useForm(form);
/* 保存编辑 */
const save = () => {
validate()
.then(() => {
loading.value = true;
const data = {
title: props.data?.appName,
parentId: props.data?.parentId,
menuType: Number(props.data?.menuType),
icon: props.data?.appIcon,
path: props.data?.path,
sortNumber: props.data?.sortNumber,
component: props.data?.component,
authority: props.data?.authority,
meta: props.data?.meta,
appId: props.data?.appId
};
installApp(data)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
watch(
() => props.visible,
(visible) => {
if (visible) {
content.value = String(props.data?.content);
} else {
resetFields();
}
}
);
</script>
<style lang="less">
.tab-pane {
min-height: 100px;
}
.card-head {
display: flex;
height: 40px;
align-items: center;
margin-bottom: 30px;
}
.pay-box {
display: flex;
.qrcode {
border: 1px solid #f3f3f3;
width: 204px;
height: auto;
}
.pay-info {
margin-left: 20px;
}
}
</style>

View File

@@ -0,0 +1,51 @@
<!-- 角色选择下拉框 -->
<template>
<a-select
optionFilterProp="label"
:options="data"
allow-clear
:value="value"
:placeholder="placeholder"
@update:value="updateValue"
@blur="onBlur"
@change="onChange"
/>
</template>
<script lang="ts" setup>
import { getDictionaryOptions } from '@/utils/common';
const emit = defineEmits<{
(e: 'update:value', value: string): void;
(e: 'blur'): void;
(e: 'change'): void;
}>();
withDefaults(
defineProps<{
value?: string;
placeholder?: string;
}>(),
{
placeholder: '请选择客户跟进状态'
}
);
// 字典数据
const data = getDictionaryOptions('customerFollowStatus');
/* 更新选中数据 */
const updateValue = (value: string) => {
emit('update:value', value);
};
/* 失去焦点 */
const onBlur = () => {
emit('blur');
};
/* 选择事件 */
const onChange = (e) => {
emit('change', e);
};
</script>

View File

@@ -0,0 +1,51 @@
<!-- 客户来源选择下拉框 -->
<template>
<a-select
optionFilterProp="label"
:options="data"
allow-clear
:value="value"
:placeholder="placeholder"
@update:value="updateValue"
@blur="onBlur"
@change="onChange"
/>
</template>
<script lang="ts" setup>
import { getDictionaryOptions } from '@/utils/common';
const emit = defineEmits<{
(e: 'update:value', value: string): void;
(e: 'blur'): void;
(e: 'change'): void;
}>();
withDefaults(
defineProps<{
value?: string;
placeholder?: string;
}>(),
{
placeholder: '请选择客户来源'
}
);
// 字典数据
const data = getDictionaryOptions('customerSource');
/* 更新选中数据 */
const updateValue = (value: string) => {
emit('update:value', value);
};
/* 失去焦点 */
const onBlur = () => {
emit('blur');
};
/* 选择事件 */
const onChange = (e) => {
emit('change', e);
};
</script>

View File

@@ -0,0 +1,45 @@
<!-- 角色选择下拉框 -->
<template>
<a-select
show-search
optionFilterProp="label"
:options="data"
allow-clear
:value="value"
:placeholder="placeholder"
@update:value="updateValue"
@blur="onBlur"
/>
</template>
<script lang="ts" setup>
import { getDictionaryOptions } from '@/utils/common';
const emit = defineEmits<{
(e: 'update:value', value: string): void;
(e: 'blur'): void;
}>();
withDefaults(
defineProps<{
value?: string;
placeholder?: string;
}>(),
{
placeholder: '请选择状态'
}
);
// 字典数据
const data = getDictionaryOptions('status');
/* 更新选中数据 */
const updateValue = (value: string) => {
emit('update:value', value);
};
/* 失去焦点 */
const onBlur = () => {
emit('blur');
};
</script>

View File

@@ -0,0 +1,44 @@
<!-- 角色选择下拉框 -->
<template>
<a-select
optionFilterProp="label"
:options="data"
allow-clear
:value="value"
:placeholder="placeholder"
@update:value="updateValue"
@blur="onBlur"
/>
</template>
<script lang="ts" setup>
import { getDictionaryOptions } from '@/utils/common';
const emit = defineEmits<{
(e: 'update:value', value: string): void;
(e: 'blur'): void;
}>();
withDefaults(
defineProps<{
value?: string;
placeholder?: string;
}>(),
{
placeholder: '请选择客户类型'
}
);
// 字典数据
const data = getDictionaryOptions('customerType');
/* 更新选中数据 */
const updateValue = (value: string) => {
emit('update:value', value);
};
/* 失去焦点 */
const onBlur = () => {
emit('blur');
};
</script>

View File

@@ -0,0 +1,76 @@
<!-- 角色选择下拉框 -->
<template>
<a-select
show-search
optionFilterProp="label"
:options="data"
allow-clear
:value="value"
:placeholder="placeholder"
@update:value="updateValue"
@search="onSearch"
@blur="onBlur"
/>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { message } from 'ant-design-vue';
import { listUsers } from '@/api/system/user';
import type { SelectProps } from 'ant-design-vue';
import { UserParam } from '@/api/system/user/model';
const emit = defineEmits<{
(e: 'update:value', value: string): void;
(e: 'blur'): void;
}>();
withDefaults(
defineProps<{
value?: string;
placeholder?: string;
}>(),
{
placeholder: '请选择客户类型'
}
);
// 字典数据
const data = ref<SelectProps['options']>([]);
/* 更新选中数据 */
const updateValue = (value: string) => {
emit('update:value', value);
};
// 默认搜索条件
const where = ref<UserParam>({});
const search = () => {
/* 获取用户列 */
listUsers({ ...where?.value })
.then((result) => {
data.value = result?.map((d) => {
return {
value: d.userId,
label: d.nickname
};
});
})
.catch((e) => {
message.error(e.message);
});
};
const onSearch = (e) => {
where.value.nickname = e;
search();
};
search();
/* 失去焦点 */
const onBlur = () => {
emit('blur');
};
</script>

View File

@@ -0,0 +1,400 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="740"
:visible="visible"
:confirm-loading="loading"
:title="isUpdate ? '编辑应用' : '创建应用'"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
:label-col="{ md: { span: 6 }, sm: { span: 4 }, xs: { span: 24 } }"
:wrapper-col="{ md: { span: 18 }, sm: { span: 20 }, xs: { span: 24 } }"
>
<a-row :gutter="16">
<a-col :md="12" :sm="24" :xs="24">
<a-form-item label="应用分类" v-bind="validateInfos.appType">
<a-select
optionFilterProp="label"
placeholder="请选择应用分类"
allow-clear
v-model:value="form.appType"
>
<template v-for="item in appTypeDict">
<a-select-option :value="item.value">{{ item.label }}</a-select-option>
</template>
</a-select>
</a-form-item>
<a-form-item label="应用名称" v-bind="validateInfos.appName">
<a-input
allow-clear
placeholder="请输入应用名称"
v-model:value="form.appName"
/>
</a-form-item>
<a-form-item label="应用入口" v-bind="validateInfos.component">
<a-input
allow-clear
placeholder="/dashboard/workplace"
v-model:value="form.component"
/>
</a-form-item>
</a-col>
<a-col :md="12" :sm="24" :xs="24">
<a-form-item label="应用标识" v-bind="validateInfos.appCode">
<a-input
allow-clear
placeholder="goods"
v-model:value="form.appCode"
/>
</a-form-item>
<a-form-item label="授权价格" v-bind="validateInfos.price">
<a-input
allow-clear
placeholder="授权价格"
v-model:value="form.price"
/>
</a-form-item>
<a-form-item label="排序号" v-role="'superAdmin'" v-bind="validateInfos.sortNumber">
<a-input-number
:min="0"
:max="99999"
class="ele-fluid"
placeholder="请输入排序号"
v-model:value="form.sortNumber"
/>
</a-form-item>
</a-col>
</a-row>
<a-form-item
label="应用简介"
v-bind="validateInfos.comments"
:label-col="{ md: { span: 3 }, sm: { span: 4 }, xs: { span: 24 } }"
:wrapper-col="{ md: { span: 21 }, sm: { span: 20 }, xs: { span: 24 } }"
>
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入应用简介"
v-model:value="form.comments"
/>
</a-form-item>
<div style="margin-bottom: 22px">
<a-divider />
</div>
<a-row :gutter="16">
<a-col :md="12" :sm="24" :xs="24">
<a-form-item label="应用图标" v-bind="validateInfos.appIcon">
<ele-icon-picker
v-model:value="form.appIcon"
allow-clear
placeholder="请选择图标"
:disabled="form.appType === 1"
:data="iconData"
>
<template #icon="{ icon }">
<component :is="icon"/>
</template>
</ele-icon-picker>
</a-form-item>
<a-form-item label="下载地址" v-bind="validateInfos.downUrl">
<a-input
allow-clear
placeholder="模块代码下载地址"
v-model:value="form.downUrl"
/>
</a-form-item>
</a-col>
<a-col :md="12" :sm="24" :xs="24">
<a-form-item label="当前版本" v-bind="validateInfos.edition">
<a-select
optionFilterProp="label"
placeholder="请选择版本"
allow-clear
v-model:value="form.edition"
>
<a-select-option value="正式版">正式版</a-select-option>
<a-select-option value="开发版">开发版</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="应用状态">
<a-switch
checked-children="启用"
un-checked-children="禁用"
:checked="form.status === 0"
@update:checked="updateHideValue"
/>
<a-tooltip
title="禁用后提示:模块维护中"
>
<question-circle-outlined
style="vertical-align: -4px; margin-left: 16px"
/>
</a-tooltip>
</a-form-item>
</a-col>
</a-row>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import {ref, reactive, watch} from 'vue';
import {Form, message} from 'ant-design-vue';
import type {RuleObject} from 'ant-design-vue/es/form';
import iconData from 'ele-admin-pro/es/ele-icon-picker/icons';
import {assignObject, isExternalLink} from 'ele-admin-pro';
import {addApp, updateApp} from '@/api/dashboard/appstore';
import type {App} from '@/api/dashboard/appstore/model';
import {ItemType} from "ele-admin-pro/es/ele-image-upload/types";
import {uploadFile} from "@/api/system/file";
import {getDictionaryOptions} from "@/utils/common";
const useForm = Form.useForm;
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: App | null;
// 上级应用id
parentId?: number;
}>();
// 是否是修改
const isUpdate = ref(false);
// 提交状态
const loading = ref(false);
const icon = ref('');
const appTypeDict = getDictionaryOptions('appstoreType');
// 编辑器内容,双向绑定
const content = ref('');
// 表单数据
const form = reactive<App>({
// 应用id
appId: undefined,
// 应用名称
appName: '',
// 上级id, 0是顶级
parentId: 0,
// 应用编号
appCode: '',
// 应用图标
appIcon: '',
// 应用类型
appType: undefined,
// 应用地址
appUrl: '',
// 下载地址
downUrl: '',
// 应用包名
packageName: '',
// 点击次数
clicks: '',
// 安装次数
installs: '',
// 应用介绍
content: '',
// 开发者(个人)
developer: '官方',
// 页面路径
component: undefined,
// 软件授权价格
price: '',
// 评分
score: '',
// 星级
star: '',
// 排序
sortNumber: 100,
// 备注
comments: '',
// 权限标识
authority: '',
// 打开位置
target: '',
// 是否隐藏, 0否, 1是(仅注册路由不显示在左侧菜单)
hide: undefined,
// 菜单侧栏选中的path
active: '',
// 其它路由元信息
meta: '',
// 版本
edition: '开发版',
// 版本号
version: 'v1.0',
// 创建时间
createTime: '',
// 状态
status: 1,
// 发布者
userId: undefined,
// 发布者昵称
nickname: ''
});
// 表单验证规则
const rules = reactive({
appName: [
{
required: true,
type: 'string',
message: '请输入应用名称',
trigger: 'blur'
}
],
component: [
{
required: true,
type: 'string',
message: '请输入组件路径',
trigger: 'blur'
}
],
appType: [
{
required: true,
type: 'string',
message: '请选择应用分类',
trigger: 'blur'
}
],
appCode: [
{
required: true,
type: 'string',
message: '请输入应用标识(英文)',
trigger: 'blur'
}
],
comments: [
{
required: true,
type: 'string',
message: '请输入应用简介',
}
],
sortNumber: [
{
required: true,
type: 'number',
message: '请输入排序号',
}
],
price: [
{
required: true,
message: '请输入软件授权价格',
trigger: 'blur'
}
],
meta: [
{
type: 'string',
trigger: 'blur',
validator: async (_rule: RuleObject, value: string) => {
if (value) {
const msg = '请输入正确的JSON格式';
try {
const obj = JSON.parse(value);
if (typeof obj !== 'object' || obj === null) {
return Promise.reject(msg);
}
} catch (_e) {
return Promise.reject(msg);
}
}
return Promise.resolve();
}
}
]
});
const {resetFields, validate, validateInfos} = useForm(form, rules);
/* 保存编辑 */
const save = () => {
validate()
.then(() => {
loading.value = true;
const appForm = {
...form,
parentId: form.parentId || 0,
content: content.value
};
const saveOrUpdate = isUpdate.value ? updateApp : addApp;
saveOrUpdate(appForm)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {
});
};
const onUpload = (d: ItemType) => {
uploadFile(<File>d.file)
.then((result) => {
form.appIcon = result.path;
message.success('上传成功');
})
.catch((e) => {
message.error(e.message);
});
};
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
const updateHideValue = (value: boolean) => {
form.status = value ? 0 : 1;
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
const isExternal = isExternalLink(props.data.path);
const isInner = isExternalLink(props.data.component);
content.value = String(props.data.content);
form.price = props.data.price;
assignObject(form, {
...props.data,
openType: isExternal ? 2 : isInner ? 1 : 0
});
isUpdate.value = true;
} else {
form.parentId = props.parentId;
isUpdate.value = false;
}
} else {
resetFields();
}
}
);
</script>
<script lang="ts">
import * as icons from '@ant-design/icons-vue';
export default {
components: icons
};
</script>

View File

@@ -0,0 +1,96 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<!-- <a-button-->
<!-- v-role="'superAdmin'"-->
<!-- type="primary"-->
<!-- class="ele-btn-icon"-->
<!-- @click="add"-->
<!-- >-->
<!-- <template #icon>-->
<!-- <plus-outlined />-->
<!-- </template>-->
<!-- <span>创建应用</span>-->
<!-- </a-button>-->
<a-input-search
allow-clear
placeholder="请输入应用名称"
v-model:value="searchText"
@pressEnter="search"
@search="search"
>
<template #addonBefore>
<a-select v-model:value="type" style="width: 100px; margin: -5px -12px">
<a-select-option value="appName">应用名称</a-select-option>
<a-select-option value="appCode">应用标识</a-select-option>
</a-select>
</template>
</a-input-search>
</a-space>
</template>
<script lang="ts" setup>
import {
PlusOutlined,
EditOutlined,
SearchOutlined,
DeleteOutlined,
UpSquareOutlined,
DownSquareOutlined
} from '@ant-design/icons-vue';
import useSearch from '@/utils/use-search';
import type { AppParam } from '@/api/dashboard/appstore/model';
import { ref, watch } from 'vue';
import { assignObject } from 'ele-admin-pro';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: AppParam): void;
(e: 'add'): void;
(e: 'remove'): void;
}>();
// 表单数据
const { where } = useSearch<AppParam>({
appName: '',
appCode: ''
});
// 下拉选项
const type = ref('appName');
// 搜索内容
const searchText = ref('');
/* 搜索 */
const search = () => {
assignObject(where, {});
if (type.value == 'appName') {
where.appName = searchText.value;
}
if (type.value == 'appCode') {
where.appCode = searchText.value;
}
emit('search', where);
};
// 新增
const add = () => {
emit('add');
};
// 批量删除
const removeBatch = () => {
emit('remove');
};
watch(
() => props.selection,
() => {}
);
</script>

View File

@@ -0,0 +1,83 @@
<!-- 用户编辑弹窗 -->
<template>
<ele-modal
:width="800"
:visible="visible"
v-if="data"
:title="`${data.appName}`"
:maxable="true"
@update:visible="updateVisible"
:footer="null"
>
<a-tabs type="card" v-model:activeKey="activeKey" tabPosition="top">
<a-tab-pane key="0" tab="插件介绍">
<Basic :data="data" :appId="data.appId" />
</a-tab-pane>
<a-tab-pane key="1" tab="菜单管理">
<Menu :data="data" :menuType="0" />
</a-tab-pane>
<a-tab-pane key="2" tab="按钮管理">
<Authority :data="data" :menuType="1" />
</a-tab-pane>
<a-tab-pane key="3" tab="插件设置">
<Setting :data="data" :appId="data.appId" />
</a-tab-pane>
</a-tabs>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import type { App } from '@/api/dashboard/appstore/model';
import Authority from './setting/authority.vue';
import Menu from './setting/menu.vue';
import Basic from './setting/basic.vue';
import Setting from './setting/setting.vue';
import { message } from 'ant-design-vue';
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: App | null;
showView?: boolean;
}>();
const emit = defineEmits<{
(e: 'update:visible', visible: boolean): void;
}>();
const activeKey = ref('0');
// 编辑器内容,双向绑定
const content = ref<any>('');
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 保存设置
const save = () => {
message.warn('待开发...');
};
watch(
() => props.visible,
(visible) => {
if (visible) {
content.value = props.data?.content;
}
}
);
</script>
<style lang="less">
.tab-pane {
min-height: 100px;
}
.card-head {
display: flex;
height: 40px;
align-items: center;
margin-bottom: 30px;
}
</style>

View File

@@ -0,0 +1,166 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="400"
:visible="visible"
:confirm-loading="loading"
:title="isUpdate ? '修改按钮' : '添加按钮'"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
:label-col="{ md: { span: 6 }, sm: { span: 4 }, xs: { span: 24 } }"
:wrapper-col="{ md: { span: 18 }, sm: { span: 20 }, xs: { span: 24 } }"
>
<a-row :gutter="16">
<a-col :md="23" :sm="24" :xs="24">
<a-form-item label="按钮名称" v-bind="validateInfos.appName">
<a-input
allow-clear
placeholder="请输入按钮名称"
v-model:value="form.appName"
/>
</a-form-item>
<a-form-item label="权限标识" v-bind="validateInfos.authority">
<a-input
allow-clear
placeholder="sys:article:list"
v-model:value="form.authority"
@pressEnter="save"
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { addApp, updateApp } from '@/api/dashboard/appstore';
import type { App } from '@/api/dashboard/appstore/model';
import { assignObject, isExternalLink } from 'ele-admin-pro';
const useForm = Form.useForm;
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: App | null;
parentId?: number;
}>();
// 提交状态
const loading = ref(false);
// 是否是修改
const isUpdate = ref(false);
// 表单数据
const form = reactive<App>({
appId: undefined,
// 菜单id
appName: '',
// 上级id, 0是顶级
parentId: 0,
// 菜单类型, 0菜单, 1按钮
menuType: 1,
// 排序号
sortNumber: 100,
// 权限标识
authority: ''
});
// 表单验证规则
const rules = reactive({
appName: [
{
required: true,
type: 'string',
message: '请输入按钮名称',
trigger: 'blur'
}
],
authority: [
{
required: true,
type: 'string',
message: '请输入权限标识',
trigger: 'blur'
}
]
});
const { resetFields, validate, validateInfos } = useForm(form, rules);
/* 保存编辑 */
const save = () => {
validate()
.then(() => {
loading.value = true;
const appForm = {
...form,
parentId: props.parentId,
appID: props.data?.appId,
menuType: Number(form.menuType),
appName: form.appName,
authority: form.authority
};
const saveOrUpdate = isUpdate.value ? updateApp : addApp;
saveOrUpdate(appForm)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data?.appId) {
const isExternal = isExternalLink(props.data.path);
const isInner = isExternalLink(props.data.component);
assignObject(form, {
...props.data,
openType: isExternal ? 2 : isInner ? 1 : 0,
parentId: props.data?.parentId
});
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
isUpdate.value = false;
}
}
);
</script>
<script lang="ts">
import * as icons from '@ant-design/icons-vue';
export default {
components: icons
};
</script>

View File

@@ -0,0 +1,172 @@
<template>
<div>
<a-space v-role="'superAdmin'" style="margin-bottom: 10px">
<a-button
type="primary"
class="ele-btn-icon"
v-role="'superAdmin'"
v-if="data"
@click="openEdit"
>
<span>添加</span>
</a-button>
</a-space>
<!-- 表格 -->
<ele-pro-table
ref="tableRef"
row-key="appId"
:columns="columns"
class="sys-org-table"
:toolbar="false"
:datasource="datasource"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'menuType'">
<a-tag v-if="record.menuType === '0'" color="blue">菜单</a-tag>
<a-tag v-else-if="record.menuType === '1'">按钮</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space v-role="'superAdmin'">
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical" />
<a-popconfirm
title="确定要删除此记录吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
</div>
<!-- 编辑弹窗 -->
<AuthorityEdit
v-model:visible="showEdit"
:data="current"
:parentId="parentId"
@done="reload"
/>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { message } from 'ant-design-vue';
import type {
ColumnItem,
DatasourceFunction
} from 'ele-admin-pro/es/ele-pro-table/types';
import type { EleProTable } from 'ele-admin-pro';
import AuthorityEdit from './authority-edit.vue';
import { App, AppParam } from '@/api/dashboard/appstore/model';
import { listApp, removeApp } from '@/api/dashboard/appstore';
const props = defineProps<{
// 菜单类型
menuType?: number;
// 修改回显的数据
data?: App | null;
showView?: boolean;
}>();
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '名称',
key: 'appName',
dataIndex: 'appName',
ellipsis: true
},
{
title: '权限标识',
dataIndex: 'authority',
ellipsis: true
},
{
title: '操作',
key: 'action',
width: 200,
align: 'center',
hideInSetting: true
}
]);
/* 表格数据源 */
const datasource: DatasourceFunction = ({ page, limit, where, orders }) => {
where.parentId = props.data?.appId;
where.menuType = props.menuType;
return listApp({ ...where, ...orders, page, limit });
};
// 当前编辑数据
const current = ref<App | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 上级菜单id
const parentId = ref<number>();
// 菜单数据
// const menuData = ref<App[]>([]);
// 表格展开的行
// const expandedRowKeys = ref<number[]>([]);
/* 表格渲染完成回调 */
// const onDone: EleProTableDone<App> = ({ data }) => {
// menuData.value = data;
// };
/* 刷新表格 */
const reload = (where?: AppParam) => {
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: App | null, id?: number) => {
parentId.value = props.data?.appId;
current.value = row ?? null;
showEdit.value = true;
};
/* 删除单个 */
const remove = (row: App) => {
if (row.children?.length) {
message.error('请先删除子节点');
return;
}
const hide = message.loading('请求中..', 0);
removeApp(row.appId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
watch(
() => props.data?.appId,
(visible) => {
if (visible) {
reload();
}
}
);
</script>
<script lang="ts">
import * as MenuIcons from '@/layout/menu-icons';
export default {
name: 'Authority',
components: MenuIcons
};
</script>

View File

@@ -0,0 +1,111 @@
<template>
<div>
<!-- 编辑器 -->
<byte-md-editor
v-model:value="content"
:locale="zh_Hans"
:plugins="plugins"
uploadImages
height="500px"
mode="split"
:editorConfig="{ lineNumbers: true }"
/>
<a-button
type="primary"
class="ele-btn-icon"
style="margin-top: 10px"
@click="saveContent"
>
<span>保存</span>
</a-button>
</div>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { message } from 'ant-design-vue';
import { App } from '@/api/dashboard/appstore/model';
import { updateApp } from '@/api/dashboard/appstore';
// 中文语言文件
import zh_Hans from 'bytemd/locales/zh_Hans.json';
// 链接、删除线、复选框、表格等的插件
import gfm from '@bytemd/plugin-gfm';
// 插件的中文语言文件
import zh_HansGfm from '@bytemd/plugin-gfm/locales/zh_Hans.json';
// 预览界面的样式,这里用的 github 的 markdown 主题
import 'github-markdown-css/github-markdown-light.css';
import highlight from '@bytemd/plugin-highlight';
import { assignObject, isExternalLink } from 'ele-admin-pro';
const props = defineProps<{
// 应用id
appId?: number | 0;
// 修改回显的数据
data?: App | null;
}>();
// 编辑器内容,双向绑定
const content = ref<any>('');
// 插件
const plugins = ref([
gfm({
locale: zh_HansGfm
}),
highlight()
]);
// 保存应用详情
const saveContent = () => {
const appForm = {
appId: props.data?.appId,
content: content.value
};
updateApp(appForm)
.then((msg) => {
message.success('保存成功');
})
.catch((e) => {
message.error(e.message);
});
};
// 加载内容
const setContent = () => {
content.value = props.data?.content;
};
const reload = () => {
if (props.data) {
const isExternal = isExternalLink(props.data.path);
const isInner = isExternalLink(props.data.component);
content.value = String(props.data.content);
} else {
}
};
setContent();
if (props.appId) {
reload();
}
watch(
() => props.data?.appId,
(data) => {
if (data) {
reload();
} else {
}
}
);
</script>
<script lang="ts">
import * as MenuIcons from '@/layout/menu-icons';
export default {
name: 'Authority',
components: MenuIcons
};
</script>

View File

@@ -0,0 +1,87 @@
<template>
<div>
<!-- 编辑器 -->
<byte-md-editor
v-model:value="content"
:locale="zh_Hans"
:plugins="plugins"
uploadImages
height="500px"
:editorConfig="{ lineNumbers: true }"
/>
<a-button
type="primary"
class="ele-btn-icon"
style="margin-top: 10px"
@click="saveContent"
>
<span>保存</span>
</a-button>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { message } from 'ant-design-vue';
import { App } from '@/api/dashboard/appstore/model';
import { updateApp } from '@/api/dashboard/appstore';
// 中文语言文件
import zh_Hans from 'bytemd/locales/zh_Hans.json';
// 链接、删除线、复选框、表格等的插件
import gfm from '@bytemd/plugin-gfm';
// 插件的中文语言文件
import zh_HansGfm from '@bytemd/plugin-gfm/locales/zh_Hans.json';
// 预览界面的样式,这里用的 github 的 markdown 主题
import 'github-markdown-css/github-markdown-light.css';
import highlight from '@bytemd/plugin-highlight';
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: App | null;
showView?: boolean;
}>();
// 编辑器内容,双向绑定
const content = ref<any>('');
// 插件
const plugins = ref([
gfm({
locale: zh_HansGfm
}),
highlight()
]);
// 保存应用详情
const saveContent = () => {
const appForm = {
appId: props.data?.appId,
content: content.value
};
updateApp(appForm)
.then((msg) => {
message.success('保存成功');
})
.catch((e) => {
message.error(e.message);
});
};
// 加载内容
const setContent = () => {
content.value = props.data?.content;
};
setContent();
</script>
<script lang="ts">
import * as MenuIcons from '@/layout/menu-icons';
export default {
name: 'Authority',
components: MenuIcons
};
</script>

View File

@@ -0,0 +1,188 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="400"
:visible="visible"
:confirm-loading="loading"
:title="isUpdate ? '修改组件' : '添加组件'"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
:label-col="{ md: { span: 6 }, sm: { span: 4 }, xs: { span: 24 } }"
:wrapper-col="{ md: { span: 18 }, sm: { span: 20 }, xs: { span: 24 } }"
>
<a-row :gutter="16">
<a-col :md="23" :sm="24" :xs="24">
<a-form-item label="组件名称" v-bind="validateInfos.appName">
<a-input
allow-clear
:placeholder="`文章详情`"
v-model:value="form.appName"
/>
</a-form-item>
<a-form-item label="组件路径" v-bind="validateInfos.path">
<a-input
allow-clear
:placeholder="`/article/detail/:id`"
v-model:value="form.path"
/>
</a-form-item>
<a-form-item label="组件路径" v-bind="validateInfos.component">
<a-input
allow-clear
:placeholder="`/article/detail`"
v-model:value="form.component"
@pressEnter="save"
/>
</a-form-item>
<a-form-item label="路由元数据" v-bind="validateInfos.meta">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入JSON格式的路由元数据"
v-model:value="form.meta"
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { addApp, updateApp } from '@/api/dashboard/appstore';
import type { App } from '@/api/dashboard/appstore/model';
import { assignObject, isExternalLink } from 'ele-admin-pro';
const useForm = Form.useForm;
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: App | null;
parentId?: number;
}>();
// 提交状态
const loading = ref(false);
// 是否是修改
const isUpdate = ref(false);
// 表单数据
const form = reactive<App>({
appId: undefined,
// 菜单id
appName: '',
// 上级id, 0是顶级
parentId: 0,
// 菜单类型, 0菜单, 1按钮
menuType: 0,
// 排序号
sortNumber: 100,
path: '',
component: '',
meta: ''
});
// 表单验证规则
const rules = reactive({
appName: [
{
required: true,
type: 'string',
message: '请输入按钮名称',
trigger: 'blur'
}
],
path: [
{
required: true,
type: 'string',
message: '请输入路由地址',
trigger: 'blur'
}
],
component: [
{
required: true,
type: 'string',
message: '请输组件路径',
trigger: 'blur'
}
]
});
const { resetFields, validate, validateInfos } = useForm(form, rules);
/* 保存编辑 */
const save = () => {
validate()
.then(() => {
loading.value = true;
const appForm = {
...form,
parentId: props.parentId,
appID: props.data?.appId,
menuType: Number(form.menuType),
appName: form.appName
};
const saveOrUpdate = isUpdate.value ? updateApp : addApp;
saveOrUpdate(appForm)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data?.appId) {
const isExternal = isExternalLink(props.data.path);
const isInner = isExternalLink(props.data.component);
assignObject(form, {
...props.data,
openType: isExternal ? 2 : isInner ? 1 : 0,
parentId: props.data?.parentId
});
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
isUpdate.value = false;
}
}
);
</script>
<script lang="ts">
import * as icons from '@ant-design/icons-vue';
export default {
components: icons
};
</script>

View File

@@ -0,0 +1,174 @@
<template>
<div>
<a-space style="margin-bottom: 10px">
<a-button
type="primary"
class="ele-btn-icon"
v-if="data"
@click="openEdit"
>
<span>添加</span>
</a-button>
</a-space>
<!-- 表格 -->
<ele-pro-table
ref="tableRef"
row-key="appId"
:columns="columns"
class="sys-org-table"
:toolbar="false"
:datasource="datasource"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'menuType'">
<a-tag v-if="record.menuType === '0'" color="blue">菜单</a-tag>
<a-tag v-else-if="record.menuType === '1'">按钮</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical" />
<a-popconfirm
title="确定要删除此记录吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
</div>
<!-- 编辑弹窗 -->
<MenuEdit
v-model:visible="showEdit"
:data="current"
:parentId="parentId"
@done="reload"
/>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { message } from 'ant-design-vue';
import type {
ColumnItem,
DatasourceFunction
} from 'ele-admin-pro/es/ele-pro-table/types';
import type { EleProTable } from 'ele-admin-pro';
import MenuEdit from './menu-edit.vue';
import { App, AppParam } from '@/api/dashboard/appstore/model';
import { listApp, removeApp } from '@/api/dashboard/appstore';
const props = defineProps<{
// 菜单类型
menuType?: number;
// 修改回显的数据
data?: App | null;
showView?: boolean;
}>();
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '组件名称',
key: 'appName',
dataIndex: 'appName',
ellipsis: true
},
{
title: '路由地址',
dataIndex: 'path'
},
{
title: '组件路径',
dataIndex: 'component'
},
{
title: '操作',
key: 'action',
width: 200,
align: 'center',
hideInSetting: true
}
]);
/* 表格数据源 */
const datasource: DatasourceFunction = ({ page, limit, where, orders }) => {
where.parentId = props.data?.appId;
where.menuType = props.menuType;
return listApp({ ...where, ...orders, page, limit });
};
// 当前编辑数据
const current = ref<App | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 上级菜单id
const parentId = ref<number>();
// 菜单数据
// const menuData = ref<App[]>([]);
// 表格展开的行
// const expandedRowKeys = ref<number[]>([]);
/* 表格渲染完成回调 */
// const onDone: EleProTableDone<App> = ({ data }) => {
// menuData.value = data;
// };
/* 刷新表格 */
const reload = (where?: AppParam) => {
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: App | null, id?: number) => {
parentId.value = props.data?.appId;
current.value = row ?? null;
showEdit.value = true;
};
/* 删除单个 */
const remove = (row: App) => {
if (row.children?.length) {
message.error('请先删除子节点');
return;
}
const hide = message.loading('请求中..', 0);
removeApp(row.appId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
watch(
() => props.data?.appId,
(visible) => {
if (visible) {
reload();
}
}
);
</script>
<script lang="ts">
import * as MenuIcons from '@/layout/menu-icons';
export default {
name: 'Authority',
components: MenuIcons
};
</script>

View File

@@ -0,0 +1,405 @@
<!-- 编辑弹窗 -->
<template>
<a-form
:label-col="{ md: { span: 6 }, sm: { span: 4 }, xs: { span: 24 } }"
:wrapper-col="{ md: { span: 18 }, sm: { span: 20 }, xs: { span: 24 } }"
>
<a-row :gutter="16">
<a-col :md="12" :sm="24" :xs="24">
<a-form-item label="应用分类" v-bind="validateInfos.appType">
<a-select
optionFilterProp="label"
placeholder="请选择应用分类"
allow-clear
v-model:value="form.appType"
>
<template v-for="item in appTypeDict">
<a-select-option :value="item.value">{{ item.label }}</a-select-option>
</template>
</a-select>
</a-form-item>
<a-form-item label="应用名称" v-bind="validateInfos.appName">
<a-input
allow-clear
placeholder="请输入应用名称"
v-model:value="form.appName"
/>
</a-form-item>
<a-form-item label="应用入口" v-bind="validateInfos.component">
<a-input
allow-clear
placeholder="/dashboard/workplace"
v-model:value="form.component"
/>
</a-form-item>
</a-col>
<a-col :md="12" :sm="24" :xs="24">
<a-form-item label="应用标识" v-bind="validateInfos.appCode">
<a-input
allow-clear
placeholder="goods"
v-model:value="form.appCode"
/>
</a-form-item>
<a-form-item label="授权价格" v-bind="validateInfos.price">
<a-input
allow-clear
placeholder="授权价格"
v-model:value="form.price"
/>
</a-form-item>
<a-form-item label="排序号" v-role="'superAdmin'" v-bind="validateInfos.sortNumber">
<a-input-number
:min="0"
:max="99999"
class="ele-fluid"
placeholder="请输入排序号"
v-model:value="form.sortNumber"
/>
</a-form-item>
</a-col>
</a-row>
<a-form-item
label="应用简介"
v-bind="validateInfos.comments"
:label-col="{ md: { span: 3 }, sm: { span: 4 }, xs: { span: 24 } }"
:wrapper-col="{ md: { span: 21 }, sm: { span: 20 }, xs: { span: 24 } }"
>
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入应用简介"
v-model:value="form.comments"
/>
</a-form-item>
<div style="margin-bottom: 22px">
<a-divider />
</div>
<a-row :gutter="16">
<a-col :md="12" :sm="24" :xs="24">
<a-form-item label="应用图标" v-bind="validateInfos.appIcon">
<ele-icon-picker
v-model:value="form.appIcon"
allow-clear
placeholder="请选择图标"
:disabled="form.appType === 1"
:data="iconData"
>
<template #icon="{ icon }">
<component :is="icon"/>
</template>
</ele-icon-picker>
</a-form-item>
<a-form-item label="下载地址" v-bind="validateInfos.downUrl">
<a-input
allow-clear
placeholder="模块代码下载地址"
v-model:value="form.downUrl"
/>
</a-form-item>
</a-col>
<a-col :md="12" :sm="24" :xs="24">
<a-form-item label="当前版本" v-bind="validateInfos.edition">
<a-select
optionFilterProp="label"
placeholder="请选择版本"
allow-clear
v-model:value="form.edition"
>
<a-select-option value="正式版">正式版</a-select-option>
<a-select-option value="开发版">开发版</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="应用状态">
<a-switch
checked-children="启用"
un-checked-children="禁用"
:checked="form.status === 0"
@update:checked="updateHideValue"
/>
<a-tooltip
title="禁用后提示:模块维护中"
>
<question-circle-outlined
style="vertical-align: -4px; margin-left: 16px"
/>
</a-tooltip>
</a-form-item>
</a-col>
</a-row>
<div style="margin-bottom: 22px">
<a-divider />
</div>
<a-button
type="primary"
class="ele-btn-icon"
@click="save"
>
<span>保存</span>
</a-button>
</a-form>
</template>
<script lang="ts" setup>
import {ref, reactive, watch} from 'vue';
import {Form, message} from 'ant-design-vue';
import type {RuleObject} from 'ant-design-vue/es/form';
import iconData from 'ele-admin-pro/es/ele-icon-picker/icons';
import {assignObject, isExternalLink} from 'ele-admin-pro';
import {addApp, updateApp} from '@/api/dashboard/appstore';
import type {App} from '@/api/dashboard/appstore/model';
import {ItemType} from "ele-admin-pro/es/ele-image-upload/types";
import {uploadFile} from "@/api/system/file";
import {getDictionaryOptions} from "@/utils/common";
const useForm = Form.useForm;
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
const props = defineProps<{
// 修改回显的数据
data?: App | null;
// 应用id
appId?: number | 0;
}>();
// 是否是修改
const isUpdate = ref(false);
// 提交状态
const loading = ref(false);
const icon = ref('');
const appTypeDict = getDictionaryOptions('appstoreType');
// 编辑器内容,双向绑定
const content = ref('');
// 表单数据
const form = reactive<App>({
// 应用id
appId: undefined,
// 应用名称
appName: '',
// 上级id, 0是顶级
parentId: 0,
// 应用编号
appCode: '',
// 应用图标
appIcon: '',
// 应用类型
appType: undefined,
// 应用地址
appUrl: '',
// 下载地址
downUrl: '',
// 应用包名
packageName: '',
// 点击次数
clicks: '',
// 安装次数
installs: '',
// 应用介绍
content: '',
// 开发者(个人)
developer: '官方',
// 页面路径
component: undefined,
// 软件授权价格
price: '',
// 评分
score: '',
// 星级
star: '',
// 排序
sortNumber: 100,
// 备注
comments: '',
// 权限标识
authority: '',
// 打开位置
target: '',
// 是否隐藏, 0否, 1是(仅注册路由不显示在左侧菜单)
hide: undefined,
// 菜单侧栏选中的path
active: '',
// 其它路由元信息
meta: '',
// 版本
edition: '开发版',
// 版本号
version: 'v1.0',
// 创建时间
createTime: '',
// 状态
status: 1,
// 发布者
userId: undefined,
// 发布者昵称
nickname: ''
});
// 表单验证规则
const rules = reactive({
appName: [
{
required: true,
type: 'string',
message: '请输入应用名称',
trigger: 'blur'
}
],
component: [
{
required: true,
type: 'string',
message: '请输入组件路径',
trigger: 'blur'
}
],
appType: [
{
required: true,
type: 'string',
message: '请选择应用分类',
trigger: 'blur'
}
],
appCode: [
{
required: true,
type: 'string',
message: '请输入应用标识(英文)',
trigger: 'blur'
}
],
comments: [
{
required: true,
type: 'string',
message: '请输入应用简介',
}
],
sortNumber: [
{
required: true,
type: 'number',
message: '请输入排序号',
}
],
price: [
{
required: true,
message: '请输入软件授权价格',
trigger: 'blur'
}
],
meta: [
{
type: 'string',
trigger: 'blur',
validator: async (_rule: RuleObject, value: string) => {
if (value) {
const msg = '请输入正确的JSON格式';
try {
const obj = JSON.parse(value);
if (typeof obj !== 'object' || obj === null) {
return Promise.reject(msg);
}
} catch (_e) {
return Promise.reject(msg);
}
}
return Promise.resolve();
}
}
]
});
const {resetFields, validate, validateInfos} = useForm(form, rules);
/* 保存编辑 */
const save = () => {
validate()
.then(() => {
loading.value = true;
const appForm = {
...form,
parentId: form.parentId || 0,
content: content.value
};
const saveOrUpdate = isUpdate.value ? updateApp : addApp;
saveOrUpdate(appForm)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {
});
};
const onUpload = (d: ItemType) => {
uploadFile(<File>d.file)
.then((result) => {
form.appIcon = result.path;
message.success('上传成功');
})
.catch((e) => {
message.error(e.message);
});
};
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
const updateHideValue = (value: boolean) => {
form.status = value ? 0 : 1;
};
const reload = () => {
if (props.data) {
const isExternal = isExternalLink(props.data.path);
const isInner = isExternalLink(props.data.component);
content.value = String(props.data.content);
form.price = props.data.price;
assignObject(form, {
...props.data,
openType: isExternal ? 2 : isInner ? 1 : 0
});
isUpdate.value = true;
} else {
isUpdate.value = false;
}
}
if( props.appId ) {
reload();
}
watch(
() => props.data?.appId,
(data) => {
if (data) {
reload();
} else {
resetFields();
}
}
);
</script>
<script lang="ts">
import * as icons from '@ant-design/icons-vue';
export default {
components: icons
};
</script>

View File

@@ -0,0 +1,268 @@
<template>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { sm: 5, xs: 24 } : { flex: '130px' }"
:wrapper-col="styleResponsive ? { sm: 19, xs: 24 } : { flex: '1' }"
>
<a-form-item label="手机号码" name="phone">
<a-input
allow-clear
:maxlength="11"
v-model:value="form.phone"
placeholder="请输入手机号码"
>
<template #addonBefore> +86 </template>
</a-input>
</a-form-item>
<a-form-item label="验证码" name="code">
<div class="login-input-group">
<a-input
placeholder="请输入验证码"
v-model:value="form.code"
:maxlength="6"
allow-clear
/>
<a-button
class="login-captcha"
:disabled="!!countdownTime"
@click="openImgCodeModal"
>
<span v-if="!countdownTime">发送验证码</span>
<span v-else>已发送 {{ countdownTime }} s</span>
</a-button>
</div>
</a-form-item>
<a-form-item
:wrapper-col="styleResponsive ? { sm: { offset: 5 } } : { offset: 4 }"
style="margin-top: 24px"
>
<a-space size="middle">
<a-button @click="back">上一步</a-button>
<a-button type="primary" :loading="loading" @click="submit">
下一步
</a-button>
</a-space>
</a-form-item>
</a-form>
<!-- 编辑弹窗 -->
<a-modal
:width="340"
:footer="null"
title="发送验证码"
v-model:visible="visible"
>
<div class="login-input-group" style="margin-bottom: 16px">
<a-input
v-model:value="imgCode"
:maxlength="5"
placeholder="请输入图形验证码"
allow-clear
@pressEnter="sendCode"
/>
<a-button class="login-captcha">
<img alt="" :src="captcha" @click="changeCaptcha" />
</a-button>
</div>
<a-button
block
size="large"
type="primary"
:loading="codeLoading"
@click="sendCode"
>
立即发送
</a-button>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue';
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import type { FormInstance, Rule } from 'ant-design-vue/es/form';
import type { StepForm } from '../model';
import { phoneReg } from 'ele-admin-pro';
import { message } from 'ant-design-vue';
import { getCaptcha, sendSmsCaptcha } from '@/api/passport/login';
import { getMobile } from '@/utils/common';
import { addUser } from '@/api/system/user';
import { User } from '@/api/system/user/model';
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 修改回显的数据
data?: StepForm | null;
}>();
const emit = defineEmits<{
(e: 'done', data: StepForm): void;
(e: 'back'): void;
}>();
// 是否显示图形验证码弹窗
const visible = ref(false);
// 图形验证码
const imgCode = ref('');
// 发送验证码按钮loading
const codeLoading = ref(false);
// 验证码倒计时时间
const countdownTime = ref(0);
// 图形验证码地址
const captcha = ref('');
const text = ref('');
// 验证码倒计时定时器
let countdownTimer: number | null = null;
const formRef = ref<FormInstance | null>(null);
/* 发送短信验证码 */
const sendCode = () => {
if (!imgCode.value) {
message.error('请输入图形验证码');
return;
}
if (text.value !== imgCode.value) {
message.error('图形验证码不正确');
return;
}
codeLoading.value = true;
sendSmsCaptcha({ phone: form.phone })
.then(() => {
message.success('短信验证码发送成功, 请注意查收!');
visible.value = false;
codeLoading.value = false;
countdownTime.value = 60;
// 开始对按钮进行倒计时
countdownTimer = window.setInterval(() => {
if (countdownTime.value <= 1) {
countdownTimer && clearInterval(countdownTimer);
countdownTimer = null;
}
countdownTime.value--;
}, 1000);
})
.catch((e) => {
codeLoading.value = false;
message.error(e.message);
});
};
// 提交状态
const loading = ref(false);
// 表单数据
const form = reactive<User>({
phone: '',
username: '',
nickname: '',
roles: [],
organizationName: '',
password: ''
});
// 表单验证规则
const rules = reactive<Record<string, Rule[]>>({
phone: [
{
pattern: phoneReg,
message: '手机号格式不正确',
type: 'string'
}
],
code: [
{
required: true,
message: '请填写短信验证码',
type: 'string',
trigger: 'blur'
}
]
});
/* 获取图形验证码 */
const changeCaptcha = () => {
// 这里演示的验证码是后端返回base64格式的形式, 如果后端地址直接是图片请参考忘记密码页面
getCaptcha()
.then((data) => {
captcha.value = data.base64;
// 实际项目后端一般会返回验证码的key而不是直接返回验证码的内容, 登录用key去验证, 你可以根据自己后端接口修改
text.value = data.text;
// 自动回填验证码, 实际项目去掉这个
// form.code = data.text;
})
.catch((e) => {
message.error(e.message);
});
};
/* 显示发送短信验证码弹窗 */
const openImgCodeModal = () => {
if (!form.phone) {
message.error('请输入手机号码');
return;
}
imgCode.value = '';
changeCaptcha();
visible.value = true;
};
const submit = () => {
if (!formRef.value) {
return;
}
formRef.value
?.validate()
.then(() => {
loading.value = true;
const addData = {
...form,
username: props.data?.username,
password: props.data?.password,
nickname: getMobile(form.phone),
roles: [{ roleId: 5 }]
};
addUser(addData)
.then(() => {
loading.value = false;
message.success('注册成功');
emit('done', form);
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
const back = () => {
emit('back');
};
</script>
<style lang="less" scoped>
/* 验证码 */
.login-input-group {
display: flex;
align-items: center;
:deep(.ant-input-affix-wrapper) {
flex: 1;
}
.login-captcha {
width: 102px;
margin-left: 10px;
padding: 0;
& > img {
width: 100%;
height: 100%;
}
}
}
</style>

View File

@@ -0,0 +1,196 @@
<template>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { sm: 5, xs: 24 } : { flex: '130px' }"
:wrapper-col="styleResponsive ? { sm: 19, xs: 24 } : { flex: '1' }"
>
<a-form-item label="账户类型" name="applyType">
<a-radio-group v-model:value="form.applyType">
<a-radio :value="0">个人账号</a-radio>
<a-radio :value="1">企业账号</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item
label="企业名称"
name="organizationName"
v-if="form.applyType === 1"
>
<a-input
placeholder="请输入企业名称"
v-model:value="form.organizationName"
:maxlength="25"
allow-clear
/>
</a-form-item>
<!-- <div style="margin-bottom: 22px" v-if="form.applyType === 1">-->
<!-- <a-divider />-->
<!-- </div>-->
<a-form-item label="登录账号" name="username">
<a-input
allow-clear
:maxlength="16"
v-model:value="form.username"
placeholder="请输入登录账号"
/>
</a-form-item>
<a-form-item label="登录密码" name="password">
<a-input
allow-clear
:maxlength="20"
type="password"
v-model:value="form.password"
placeholder="请输入密码"
/>
</a-form-item>
<a-form-item label="确认密码" name="password2">
<a-input
allow-clear
:maxlength="20"
type="password"
v-model:value="form.password2"
placeholder="请输入确认密码"
/>
</a-form-item>
<!-- <a-form-item label=" " name="register" v-if="form.applyType === 1">-->
<!-- 我已阅读并同意 <a>用户协议</a> <a>隐私权政策</a>-->
<!-- </a-form-item>-->
<a-form-item
:wrapper-col="styleResponsive ? { sm: { offset: 5 } } : { offset: 4 }"
>
<a-button type="primary" :loading="loading" @click="submit">
下一步
</a-button>
</a-form-item>
</a-form>
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue';
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import type { FormInstance, Rule } from 'ant-design-vue/es/form';
import type { StepForm } from '../model';
import type { RuleObject } from 'ant-design-vue/es/form';
import { createCode } from '@/utils/common';
import type { User } from '@/api/system/user/model';
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const emit = defineEmits<{
(e: 'done', data: StepForm): void;
}>();
const formRef = ref<FormInstance | null>(null);
// 提交状态
const loading = ref(false);
// 表单数据
const form = reactive<User>({
applyType: 0,
phone: '',
username: createCode(),
nickname: '',
organizationName: '',
password: '',
password2: ''
});
// 表单验证规则
const rules = reactive<Record<string, Rule[]>>({
applyType: [
{
required: true,
message: '请选择账户类型',
type: 'number',
trigger: 'blur'
}
],
username: [
{
required: true,
message: '请填写登录账号',
type: 'string',
trigger: 'blur'
}
],
password: [
{
required: true,
message: '密码组合应该包含字母、数字并且长度不少于8位',
min: 8,
type: 'string',
trigger: 'blur'
}
],
password2: [
{
required: true,
validator: async (_rule: RuleObject, value: string) => {
if (!value) {
return Promise.reject('请再次输入新密码');
}
if (value !== form.password) {
return Promise.reject('两次输入密码不一致');
}
return Promise.resolve();
}
}
],
organizationName: [
{
required: true,
validator: async (_rule: RuleObject, value: string) => {
if (!value) {
return Promise.reject('请输入企业名称');
}
return Promise.resolve();
}
}
]
});
/* 步骤一提交 */
const submit = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
setTimeout(() => {
loading.value = false;
emit('done', form);
}, 300);
})
.catch(() => {});
};
</script>
<style lang="less" scoped>
/* 验证码 */
.login-input-group {
display: flex;
align-items: center;
:deep(.ant-input-affix-wrapper) {
flex: 1;
}
.login-captcha {
width: 102px;
margin-left: 10px;
padding: 0;
& > img {
width: 100%;
height: 100%;
}
}
}
</style>

View File

@@ -0,0 +1,32 @@
<template>
<div>
<a-result
title="注册成功"
status="success"
sub-title="请妥善保管您的账号如忘记可通过手机短信验证码找回密码"
/>
</div>
</template>
<script lang="ts" setup>
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import type { StepForm } from '../model';
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
defineProps<{
// 修改回显的数据
data?: StepForm | null;
}>();
const emit = defineEmits<{
(e: 'back'): void;
}>();
const back = () => {
emit('back');
};
</script>

View File

@@ -0,0 +1,112 @@
<template>
<ele-modal
:width="740"
:visible="visible"
:confirm-loading="loading"
:title="`安装应用`"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
:footer="null"
@ok="save"
>
<a-page-header :ghost="false" :title="data.appName">
<div class="ele-text-secondary" v-html="data.comments"> </div>
</a-page-header>
<div class="ele-body">
<a-card :bordered="false">
<div style="max-width: 700px; margin: 0 auto">
<div style="margin: 10px 0 30px 0">
<a-steps
:current="active"
direction="horizontal"
:responsive="styleResponsive"
>
<a-step title="第一步" description="下载更新包" />
<a-step title="第二步" description="数据初始化" />
<a-step title="第三步" description="安装成功" />
</a-steps>
</div>
<div class="step-next" v-if="active === 0">
<a-button type="primary" @click="onDone()"> 下一步 </a-button>
</div>
<div class="step-next" v-if="active === 1">
<a-button type="primary" @click="onNext()"> 下一步 </a-button>
</div>
<div class="step-next" v-if="active === 2">
<a-button type="primary" @click="onSuccess()"> 完成 </a-button>
</div>
</div>
</a-card>
</div>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue';
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import StepEdit from './components/step-edit.vue';
import StepConfirm from './components/step-confirm.vue';
import StepSuccess from './components/step-success.vue';
import type { StepForm } from './model';
import { App } from '@/api/dashboard/appstore/model';
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: App | null;
// 上级应用id
parentId?: number;
}>();
// 选中步骤
const active = ref(0);
//
const form = reactive<StepForm>({});
//
const onDone = (data: StepForm) => {
Object.assign(form, data);
active.value = 1;
};
//
const onNext = (data: StepForm) => {
Object.assign(form, data);
active.value = 2;
};
const onSuccess = () => {};
//
const onBack = () => {
active.value = 0;
};
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
</script>
<script lang="ts">
export default {
name: 'FormStep'
};
</script>
<style lang="less" scoped>
.step-next {
text-align: center;
}
</style>

View File

@@ -0,0 +1,11 @@
export interface StepForm {
applyType?: number;
apply?: string;
username?: string;
password?: string;
password2?: string;
phone?: string;
code?: string;
nickname?: string;
companyName?: string;
}

View File

@@ -0,0 +1,54 @@
<template>
<div class="page">
<a-page-header :ghost="false" title="扩展插件">
<div class="ele-text-secondary"> 通过扩展插件可以满足更多个性化需求 </div>
</a-page-header>
<div class="ele-body">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<a-tabs type="card" tabPosition="top" v-model:activeKey="activeKey">
<a-tab-pane v-for="(d, index) in data" :key="index" :tab="d.label">
<list :activeKey="activeKey" :type="d" />
</a-tab-pane>
</a-tabs>
</a-card>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import List from './list.vue';
import { appstoreType } from '@/api/system/appstore';
// 加载状态
const loading = ref(true);
// 当前选项卡
const activeKey = ref(0);
// 获取字典数据
const data = appstoreType();
/* 查询 */
const query = () => {
loading.value = true;
};
query();
</script>
<script lang="ts">
export default {
name: 'AppStore'
};
</script>
<style lang="less" scoped>
.sys-organization-list {
padding: 12px 6px;
height: calc(100vh - 242px);
border-width: 1px;
border-style: solid;
overflow: auto;
}
</style>

View File

@@ -0,0 +1,508 @@
<template>
<div class="ele-body">
<!-- 表格 -->
<ele-pro-table
ref="tableRef"
row-key="appId"
:columns="columns"
:datasource="datasource"
:expanded-row-keys="expandedRowKeys"
:expand-icon-column-index="1"
tool-class="ele-toolbar-form"
:scroll="{ x: 800 }"
class="sys-org-table"
@done="onDone"
@expand="onExpand"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'appName'">
<component :is="record.appIcon" class="ant-menu-item-icon" />
<a-tooltip title="插件详情">
<span
class="app-name cursor-pointer ele-text-primary"
@click="openSetting(record)"
>{{ record.appName }}</span
>
</a-tooltip>
</template>
<template v-if="column.key === 'appType'">
<a-tag v-if="isExternalLink(record.path)" color="orange">
外链
</a-tag>
<a-tag v-else-if="isExternalLink(record.component)" color="green">
内链
</a-tag>
<a-tag v-else-if="record.menuType === 0" color="blue">菜单</a-tag>
<a-tag v-else-if="record.menuType === 1">按钮</a-tag>
</template>
<template v-if="column.key === 'nickname'">
<a-tooltip :title="`${record.nickname}`">
<a-avatar :src="record.userAvatar" size="small" />
</a-tooltip>
</template>
<template v-if="column.key === 'price'">
<span class="ele-text-warning" v-if="record.price > 0"
>{{ formatNumber(record.price) }}</span
>
<span class="ele-text-success" v-else>免费</span>
</template>
<template v-if="column.key === 'createTime'">
<a-tooltip :title="`${toDateString(record.createTime)}`">
{{ timeAgo(record.createTime) }}
</a-tooltip>
</template>
<template v-if="column.key === 'edition'">
<div v-if="record.edition == '正式版'" class="ele-text-success">
正式版
</div>
<span v-if="record.edition == '开发版'" class="ele-text-info">
开发版
<a-tooltip
:title="`${record.version}`"
@click="openUrl('https://www.gxwebsoft.com')"
>
<question-circle-outlined style="margin-left: 16px" />
</a-tooltip>
</span>
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">正常</a-tag>
<a-tag v-if="record.status === 1" color="red">待审核</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space v-role="'admin'">
<!-- 已安装 -->
<template v-if="appIds.indexOf(record.appId) > -1">
<a @click="uninstallApp(record)">卸载</a>
</template>
<!-- 未安装 -->
<template v-else>
<template v-if="record.price > 0">
<a class="ele-text-warning" @click="onBuy(record)">购买</a>
</template>
<template v-else>
<a @click="install(record)">安装</a>
</template>
</template>
</a-space>
<!-- 开发者权限 -->
<a-space v-permission="'sys:user:add'">
<a @click="openSetting(record)">编辑</a>
<a-divider type="vertical" />
<!-- <a @click="openEdit(record)" class="ele-text-danger">编辑</a>-->
<!-- <a-divider type="vertical" />-->
<a-popconfirm
title="确定要删除此记录吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
<!-- 编辑弹窗 -->
<Edit v-model:visible="showEdit" :data="current" @done="reload" />
<!-- 应用设置 -->
<Setting v-model:visible="showSetting" :data="current" @done="reload" />
<!-- 购买弹窗 -->
<Buy v-model:visible="showBuy" :data="current" @done="reload" />
<!-- 安装弹窗 -->
<Step v-model:visible="showStep" :data="current" @done="reload" />
</div>
</template>
<script lang="ts" setup>
import { computed, createVNode, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import {
formatNumber,
toDateString,
isExternalLink,
messageLoading
} from 'ele-admin-pro';
import Search from './components/search.vue';
import Edit from './components/edit.vue';
import Buy from './components/buy.vue';
import Step from './components/step/index.vue';
import Setting from './components/setting.vue';
import type { EleProTable } from 'ele-admin-pro';
import {
listApp,
pageApp,
removeApp,
removeBatchApp,
saveMenu
} from '@/api/dashboard/appstore';
import { timeAgo } from 'ele-admin-pro';
import type { App, AppParam } from '@/api/dashboard/appstore/model';
import { useUserStore } from '@/store/modules/user';
import { EleProTableDone } from 'ele-admin-pro/es/ele-pro-table/types';
import { openUrl } from '@/utils/common';
import type { Menu } from '@/api/system/menu/model';
// import type { RoleMenu } from '@/api/system/menu/model/role-menu';
const userStore = useUserStore();
// 当前用户信息
const loginUser = computed(() => userStore.info ?? {});
// const appType = localStorage.getItem('AppType');
const props = defineProps<{
// 机构 id
type?: any;
// 当前选项卡
activeKey?: any;
}>();
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<App[]>([]);
// 当前编辑数据
const current = ref<App | null>(null); // 当前编辑数据
// 菜单数据
const menuData = ref<App[]>([]);
const expandedRowKeys = ref();
const appIds = ref<any>([]);
// 上级菜单id
const parentId = ref<number>();
// 获取字典数据
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示设置弹窗
const showSetting = ref(false);
// 是否显示购买弹窗
const showBuy = ref(false);
const showStep = ref(false);
const authorityData = ref<Menu[]>([] || null);
// const roleMenuData = ref<RoleMenu[]>([] || null);
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.progress = filters.progress;
where.appstoreSource = filters.appstoreSource;
where.appType = filters.appType;
where.status = filters.status;
where.edition = filters.edition;
}
// 搜索条件
if (props.activeKey > 0) {
// 指定应用分类
where.appType = props.type.value;
} else {
// 全部应用
where.appType = undefined;
}
where.menuType = 0;
where.parentId = 0;
return pageApp({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
key: 'index',
width: 48,
align: 'center',
fixed: 'left',
hideInSetting: true,
customRender: ({ index }) => index + (tableRef.value?.tableIndex ?? 0)
},
{
title: '应用名称',
dataIndex: 'appName',
key: 'appName',
ellipsis: true
},
{
title: '应用标识',
dataIndex: 'appCode'
},
{
title: '备注',
dataIndex: 'comments',
ellipsis: true
},
{
title: '价格',
dataIndex: 'price',
key: 'price'
},
{
title: '开发商',
dataIndex: 'developer',
hideInTable: true
},
{
title: '版本号',
key: 'edition',
filters: [
{
text: '正式版',
value: '正式版'
},
{
text: '开发版',
value: '开发版'
}
],
filterMultiple: false
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
align: 'center',
hideInTable: true,
width: 90
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
sorter: true,
ellipsis: true,
hideInTable: true,
customRender: ({ text }) => toDateString(text)
},
{
title: '操作',
key: 'action',
width: 200,
align: 'center',
hideInSetting: true
}
]);
/* 表格渲染完成回调 */
const onDone: EleProTableDone<App> = ({ data }) => {
menuData.value = data;
};
/* 安装应用 */
const install2 = (row?: App) => {
current.value = row ?? null;
showStep.value = true;
};
// 安装与卸载软件
const install = (data) => {
Modal.confirm({
title: '提示',
content: '确定要安装吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = messageLoading('模块安装中...', 0);
saveMenu(data)
.then((menuId) => {
// 获取权限列表
setTimeout(() => {
listApp({ parentId: data.appId }).then((list) => {
authorityData.value = list.map((d) => {
return {
title: d.appName,
menuType: d.menuType,
authority: d.authority,
parentId: menuId
};
});
});
}, 1000);
// 收集菜单ID
setTimeout(() => {
// saveAuthority(authorityData.value).then((response) => {
// roleMenuData.value = response?.data.map((d) => {
// return {
// menuId: d.menuId,
// roleId: 22
// };
// });
// });
}, 1000);
// 添加应用权限
setTimeout(() => {
// saveRoleMenu(roleMenuData.value).then((result) => {
// hide();
// message.success('安装成功');
// });
}, 1000);
})
.catch(() => {
hide();
message.error('安装失败 请勿重复安装');
});
}
});
};
const uninstallApp = (data) => {
console.log(data);
// removeMenu(data);
};
/* 点击展开图标时触发 */
const onExpand = (expanded: boolean, record: App) => {
if (expanded) {
expandedRowKeys.value = [
...expandedRowKeys.value,
record.appId as number
];
} else {
expandedRowKeys.value = expandedRowKeys.value.filter(
(d) => d !== record.appId
);
}
};
/* 搜索 */
const reload = (where?: AppParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: App) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开设置弹窗 */
const openSetting = (row?: App) => {
current.value = row ?? null;
showSetting.value = true;
};
/* 进入应用 */
const enterApp = (row?: App) => {
openUrl(row?.component);
};
/* 购买应用 */
const onBuy = (row?: App) => {
current.value = row ?? null;
showBuy.value = true;
};
/* 打开应用详情弹窗 */
// const openDetail = (row?: App) => {
// current.value = row ?? null;
// showInfo.value = true;
// };
/* 删除单个 */
const remove = (row: App) => {
const hide = message.loading('请求中..', 0);
removeApp(row.appId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchApp(
selection.value.map((d) => {
if (loginUser.value.userId === d.userId) {
return d.appId;
}
})
)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
// 已安装的应用
const getAppIds = () => {
// listMenus({}).then((res) => {
// console.log(res, 'sdfsdfsdf>>>>');
// });
appIds.value =
loginUser.value.authorities
?.filter((d) => !!d.appId && d.appId > 0)
?.map((d) => d.appId) ?? [];
};
getAppIds();
</script>
<script lang="ts">
import * as MenuIcons from './menu-icons';
export default {
name: 'App',
components: MenuIcons
};
</script>
<style lang="less" scoped>
.app-name {
margin-left: 5px;
margin-right: 5px;
}
.cursor-pointer {
cursor: pointer;
}
.sys-org-table :deep(.ant-table-body) {
overflow: auto !important;
overflow: overlay !important;
}
.sys-org-table :deep(.ant-table-pagination.ant-pagination) {
padding: 0 4px;
margin-bottom: 0;
}
</style>

View File

@@ -0,0 +1,56 @@
/** 菜单用到的图标 */
export {
HomeOutlined,
SettingOutlined,
TeamOutlined,
DesktopOutlined,
FileTextOutlined,
TableOutlined,
AppstoreOutlined,
CheckCircleOutlined,
ExclamationCircleOutlined,
UserOutlined,
TagOutlined,
IdcardOutlined,
BarChartOutlined,
AuditOutlined,
PicLeftOutlined,
CloseCircleOutlined,
QuestionCircleOutlined,
SoundOutlined,
ApartmentOutlined,
DashboardOutlined,
OneToOneOutlined,
DragOutlined,
InteractionOutlined,
BankOutlined,
BlockOutlined,
CheckSquareOutlined,
ProfileOutlined,
WarningOutlined,
FolderOutlined,
YoutubeOutlined,
ControlOutlined,
EllipsisOutlined,
CalendarOutlined,
AppstoreAddOutlined,
FileSearchOutlined,
EnvironmentOutlined,
CompassOutlined,
FontSizeOutlined,
SketchOutlined,
BgColorsOutlined,
PrinterOutlined,
QrcodeOutlined,
BarcodeOutlined,
PictureOutlined,
LinkOutlined,
AlertOutlined,
HistoryOutlined,
ChromeOutlined,
CodeOutlined,
ReadOutlined,
LaptopOutlined,
ShoppingCartOutlined,
SkinOutlined
} from '@ant-design/icons-vue';

View File

@@ -0,0 +1,165 @@
<!-- 缓存编辑弹窗 -->
<template>
<ele-modal
:width="460"
:visible="visible"
:confirm-loading="loading"
:title="isUpdate ? '修改缓存' : '添加缓存'"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 5, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
>
<a-form-item label="KEY" name="key">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入缓存名称"
v-model:value="form.key"
/>
</a-form-item>
<a-form-item label="CONTENT" name="content">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入KEY的值"
v-model:value="form.content"
/>
</a-form-item>
<a-form-item label="过期时间" name="expireTime">
<a-input-number
placeholder="默认永不过期"
style="width: 200px"
v-model:value="form.expireTime"
/>
<span class="ml-10">分钟</span>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { message } from 'ant-design-vue/es';
import type { FormInstance, Rule } from 'ant-design-vue/es/form';
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import useFormData from '@/utils/use-form-data';
import { getCache, updateCache } from '@/api/system/cache';
import type { Cache } from '@/api/system/cache/model';
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Cache | null;
}>();
//
const formRef = ref<FormInstance | null>(null);
// 是否是修改
const isUpdate = ref(false);
// 提交状态
const loading = ref(false);
// 表单数据
const { form, resetFields, assignFields } = useFormData<Cache>({
key: undefined,
content: '',
expireTime: undefined
});
// 表单验证规则
const rules = reactive<Record<string, Rule[]>>({
key: [
{
required: true,
message: '请输入KEY',
type: 'string',
trigger: 'blur'
}
],
content: [
{
required: true,
message: '请输入KEY的值',
type: 'string',
trigger: 'blur'
}
]
});
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
updateCache(form)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
assignFields(props.data);
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
formRef.value?.clearValidate();
}
}
);
</script>
<style lang="less">
.tab-pane {
min-height: 300px;
}
.ml-10 {
margin-left: 5px;
}
</style>

View File

@@ -0,0 +1,233 @@
<!-- 用户编辑弹窗 -->
<template>
<ele-modal
width="400px"
:visible="visible"
:confirm-loading="loading"
:title="'手机验证'"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
:maskClosable="false"
@ok="save"
>
<a-form class="login-form">
<a-form-item label="绑定的手机号码" name="phone">
{{ getMobile(form.phone) }}
</a-form-item>
<a-form-item label="校验码" name="code">
<div class="login-input-group">
<a-input
allow-clear
type="text"
:maxlength="6"
v-model:value="form.code"
>
</a-input>
<a-button
class="login-captcha"
:disabled="!!countdownTime"
@click="openImgCodeModal"
>
<span v-if="!countdownTime">发送验证码</span>
<span v-else>已发送 {{ countdownTime }} s</span>
</a-button>
</div>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch, computed, onBeforeUnmount } from "vue";
import { Form, message, Modal, SelectProps } from "ant-design-vue";
import { useUserStore } from "@/store/modules/user";
import type { AccessKey } from "@/api/system/access-key/model";
import { addAccessKey, updateAccessKey } from "@/api/system/access-key";
import { FILE_SERVER } from "@/config/setting";
import { uploadFile } from "@/api/system/file";
import { RuleObject } from "ant-design-vue/es/form";
import { isImage } from "@/utils/common";
import { listUsers } from '@/api/system/user';
import { getMobile } from '@/utils/common';
import { sendSmsCaptcha } from '@/api/login';
const useForm = Form.useForm;
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: AccessKey | null;
}>();
const userStore = useUserStore();
// 当前登录用户信息
const loginUser = computed(() => userStore.info ?? {});
// 是否是修改
const isUpdate = ref(false);
const disabled = ref(false);
// 选项卡位置
const activeKey = ref("1");
const promoter = ref<any>(undefined);
const commander = ref(undefined);
const appid = ref(undefined);
/* 打开选择弹窗 */
const content = ref("");
// 图形验证码地址
const captcha = ref("");
// 验证码倒计时定时器
let countdownTimer: number | null = null;
// 验证码倒计时时间
const countdownTime = ref(0);
// 图形验证码
const imgCode = ref("");
// 发送验证码按钮loading
const codeLoading = ref(false);
const emit = defineEmits<{
(e: "done", form: AccessKey): void;
(e: "update:visible", value: boolean): void;
}>();
// 已上传数据, 可赋初始值用于回显
const avatar = ref(<any>[]);
// 提交状态
const loading = ref(false);
// 用户信息
const form = reactive<AccessKey>({
id: 0,
phone: "",
accessKey: "",
accessSecret: "",
code: undefined,
createTime: ""
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit("update:visible", value);
};
// 表单验证规则
const rules = reactive({
name: [
{
required: true,
type: "string",
message: "请输入工单名称",
trigger: "blur"
}
],
taskType: [
{
required: true,
type: "string",
message: "请选择工单类型",
trigger: "blur"
}
],
content: [
{
required: true,
type: "string",
message: "请输入工单内容",
trigger: "blur",
validator: async (_rule: RuleObject, value: string) => {
if (content.value == "") {
return Promise.reject("请输入文字内容");
}
return Promise.resolve();
}
}
]
});
/* 显示发送短信验证码弹窗 */
const openImgCodeModal = () => {
if (!form.phone) {
message.error("手机号码有误");
return;
}
// imgCode.value = "";
sendCode();
// visible.value = true;
};
/* 发送短信验证码 */
const sendCode = () => {
codeLoading.value = true;
sendSmsCaptcha({ phone: form.phone }).then((res) => {
console.log(res);
message.success("短信验证码发送成功, 请注意查收!");
codeLoading.value = false;
countdownTime.value = 30;
// 开始对按钮进行倒计时
countdownTimer = window.setInterval(() => {
if (countdownTime.value <= 1) {
countdownTimer && clearInterval(countdownTimer);
countdownTimer = null;
}
countdownTime.value--;
}, 1000);
});
};
onBeforeUnmount(() => {
countdownTimer && clearInterval(countdownTimer);
});
const { validate, validateInfos } = useForm(form, rules);
/* 保存编辑 */
const save = () => {
validate()
.then(() => {
updateVisible(false);
const { code,phone } = form;
emit("done", { code,phone });
})
.catch(() => {
});
};
const query = () => {
listUsers({username: 'admin'}).then(res => {
form.phone = res[0].phone;
})
}
query();
</script>
<style lang="less" scoped>
.login-form{
padding: 0 20px;
}
.login-form-right .login-form {
margin: 0 15% 0 auto;
}
.login-form-left .login-form {
margin: 0 auto 0 15%;
}
/* 验证码 */
.login-input-group {
display: flex;
align-items: center;
:deep(.ant-input-affix-wrapper) {
flex: 1;
}
.login-captcha {
margin-left: 10px;
padding: 0 10px;
& > img {
width: 100%;
height: 100%;
}
}
}
</style>

167
src/views/system/cache/index.vue vendored Normal file
View File

@@ -0,0 +1,167 @@
<template>
<div class="page">
<a-page-header :ghost="false" title="缓存管理">
<div class="ele-text-secondary">
启用缓存可以缓解服务器压力提升加载速度
</div>
</a-page-header>
<div class="ele-body">
<a-card :bordered="false">
<!-- 表格 -->
<ele-pro-table
ref="tableRef"
row-key="key"
:columns="columns"
:datasource="datasource"
:where="defaultWhere"
cache-key="userBalanceLogTable"
>
<template #toolbar>
<a-space>
<a-button type="primary" class="ele-btn-icon" @click="add">
<template #icon>
<plus-outlined />
</template>
<span>添加</span>
</a-button>
<a-button @click="reset">刷新</a-button>
</a-space>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'content'">
<div class="ele-text-secondary">
<a-tooltip placement="topLeft">
<template #title>{{ record.content }}</template>
{{ record.content }}
</a-tooltip>
</div>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="remove(record)">删除</a>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
</div>
<!-- 编辑弹窗 -->
<CacheEdit v-model:visible="showEdit" :data="current" @done="reload" />
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, createVNode } from 'vue';
import { message, Modal } from 'ant-design-vue/es';
import type { EleProTable } from 'ele-admin-pro/es';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import CacheEdit from './components/cache-edit.vue';
import { updateCache, listCache, removeCache } from '@/api/system/cache';
import { Cache, CacheParam } from '@/api/system/cache/model';
import { App } from '@/api/app/model';
import {
PlusOutlined,
ExclamationCircleOutlined
} from '@ant-design/icons-vue';
import { messageLoading } from 'ele-admin-pro';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: 'KEY',
dataIndex: 'key'
},
{
title: 'CONTENT',
dataIndex: 'content',
key: 'content',
ellipsis: true
},
{
title: '操作',
key: 'action',
width: 200,
align: 'center',
hideInSetting: true
}
]);
// 表格选中数据
const selection = ref<Cache[]>([]);
const searchText = ref('');
const userId = ref<number>(0);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 当前编辑数据
const current = ref<App | null>(null);
// 默认搜索条件
const defaultWhere = reactive({
code: '',
phone: '',
username: '',
nickname: '',
userId: undefined
});
// 表格数据源
const datasource: DatasourceFunction = () => {
return listCache({ key: '*' });
};
const reset = () => {
userId.value = 0;
searchText.value = '';
reload();
};
/* 搜索 */
const reload = (where?: CacheParam) => {
selection.value = [];
tableRef?.value?.reload({ page: 1, where });
};
const add = () => {
showEdit.value = true;
// setCache({})
// .then((res) => {
// reload();
// })
// .catch((err) => {
// message.error(err.message);
// });
};
/* 删除单个 */
const remove = (row: Cache) => {
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = messageLoading('请求中..', 0);
removeCache(row.key)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
</script>
<script lang="ts">
export default {
name: 'CacheIndex'
};
</script>

View File

@@ -0,0 +1,138 @@
<!-- 最新动态 -->
<template>
<a-card :title="title" :bordered="false" :body-style="{ padding: '6px 0' }">
<template #extra>
<more-icon @remove="onRemove" @edit="onEdit" />
</template>
<div
style="height: 346px; padding: 22px 20px 0 20px"
class="ele-scrollbar-hover"
>
<a-timeline>
<a-timeline-item
v-for="item in activities"
:key="item.id"
:color="item.color"
>
<em>{{ item.time }}</em>
<em>{{ item.title }}</em>
</a-timeline-item>
</a-timeline>
</div>
</a-card>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import MoreIcon from './more-icon.vue';
defineProps<{
title?: string;
}>();
const emit = defineEmits<{
(e: 'remove'): void;
(e: 'edit'): void;
}>();
interface Activitie {
id: number;
title: string;
time: string;
color?: string;
}
// 最新动态数据
const activities = ref<Activitie[]>([]);
/* 查询最新动态 */
const queryActivities = () => {
activities.value = [
{
id: 1,
title: 'SunSmile 解决了bug 登录提示操作失败',
time: '20:30',
color: 'gray'
},
{
id: 2,
title: 'Jasmine 解决了bug 按钮颜色与设计不符',
time: '19:30',
color: 'gray'
},
{
id: 3,
title: '项目经理 指派了任务 解决项目一的bug',
time: '18:30'
},
{
id: 4,
title: '项目经理 指派了任务 解决项目二的bug',
time: '17:30'
},
{
id: 5,
title: '项目经理 指派了任务 解决项目三的bug',
time: '16:30'
},
{
id: 6,
title: '项目经理 指派了任务 解决项目四的bug',
time: '15:30',
color: 'gray'
},
{
id: 7,
title: '项目经理 指派了任务 解决项目五的bug',
time: '14:30',
color: 'gray'
},
{
id: 8,
title: '项目经理 指派了任务 解决项目六的bug',
time: '12:30',
color: 'gray'
},
{
id: 9,
title: '项目经理 指派了任务 解决项目七的bug',
time: '11:30'
},
{
id: 10,
title: '项目经理 指派了任务 解决项目八的bug',
time: '10:30',
color: 'gray'
},
{
id: 11,
title: '项目经理 指派了任务 解决项目九的bug',
time: '09:30',
color: 'green'
},
{
id: 12,
title: '项目经理 指派了任务 解决项目十的bug',
time: '08:30',
color: 'red'
}
];
};
const onRemove = () => {
emit('remove');
};
const onEdit = () => {
emit('edit');
};
queryActivities();
</script>
<style lang="less" scoped>
.ele-scrollbar-hover
:deep(.ant-timeline-item-last > .ant-timeline-item-content) {
min-height: auto;
}
</style>

View File

@@ -0,0 +1,82 @@
<template>
<a-card :title="title" :bordered="false" :body-style="{ padding: '2px' }">
<template #extra
><a @click="openUrl('/oa/app/index')" class="ele-text-placeholder"
>更多<RightOutlined /></a
></template>
<a-list :size="`small`" :split="false" :data-source="list">
<template #renderItem="{ item }">
<a-list-item>
<div class="app-box">
<a-image
:height="45"
:width="45"
:preview="false"
:src="item.appIcon"
fallback="https://file.wsdns.cn/20230218/550e610d43334dd2a7f66d5b20bd58eb.svg"
/>
<div class="app-info">
<a
class="ele-text-heading"
@click="openNew('/oa/app/detail/' + item.appId)"
>
{{ item.appName }}
</a>
<span class="ele-text-placeholder">
{{ item.appCode }}
</span>
</div>
</div>
</a-list-item>
</template>
</a-list>
</a-card>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { Article } from '@/api/cms/article/model';
import { openUrl } from '@/utils/common';
import { pageApp } from '@/api/oa/app';
import { RightOutlined } from '@ant-design/icons-vue';
const props = defineProps<{
title: string;
}>();
const list = ref<Article[]>([]);
/**
* 加载数据
*/
const reload = () => {
const { title } = props;
// 加载文章列表
pageApp({
limit: 5,
status: 0,
appStatus: '开发中'
}).then((data) => {
if (data?.list) {
list.value = data.list;
}
});
};
reload();
</script>
<script lang="ts">
export default {
name: 'DashboardArticleList'
};
</script>
<style lang="less" scoped>
.app-box {
display: flex;
.app-info {
display: flex;
margin-left: 5px;
flex-direction: column;
}
}
</style>

View File

@@ -0,0 +1,56 @@
<template>
<a-card :title="title" :bordered="false" :body-style="{ padding: '2px' }">
<template #extra
><a
@click="openNew('/cms/category/' + categoryId)"
class="ele-text-placeholder"
>更多<RightOutlined /></a
></template>
<a-list :size="`small`" :split="false" :data-source="list">
<template #renderItem="{ item }">
<a-list-item>
<a
class="ele-text-secondary"
@click="openUrl('/cms/article/' + item.articleId)"
>
{{ item.title }}
</a>
</a-list-item>
</template>
</a-list>
</a-card>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { pageArticle } from '@/api/cms/article';
import { Article } from '@/api/cms/article/model';
import { openNew, openUrl } from '@/utils/common';
import { RightOutlined } from '@ant-design/icons-vue';
const list = ref<Article[]>([]);
const props = defineProps<{
title: string;
categoryId: number;
}>();
/**
* 加载数据
*/
const reload = () => {
const { categoryId } = props;
// 加载文章列表
pageArticle({ categoryId, limit: 6 }).then((data) => {
if (data?.list) {
list.value = data.list;
}
});
};
reload();
</script>
<script lang="ts">
export default {
name: 'DashboardArticleList'
};
</script>

View File

@@ -0,0 +1,80 @@
<template>
<a-card :bordered="false" title="小组成员">
<template #extra>
<a-tooltip>
<template #title>邀请加入</template>
<UserAddOutlined @click="onShowQrcode" :style="{ fontSize: '18px' }" />
</a-tooltip>
</template>
<a-list
class="demo-loadmore-list"
item-layout="horizontal"
:data-source="list"
>
<template #renderItem="{ item }">
<a-list-item>
<template #actions>
<a-popover>
<template #content> 待处理 </template>
<a class="ele-text-danger">{{ item.pending }}</a>
</a-popover>
<a-popover>
<template #content> 本月已处理 </template>
<a class="ele-text-secondary">{{ item.month }}</a>
</a-popover>
</template>
<a-skeleton avatar :title="false" :loading="!!item.loading" active>
<a-list-item-meta>
<template #title>
{{ item.nickname }}
</template>
<template #avatar>
<a-avatar :src="item.avatar" />
</template>
</a-list-item-meta>
</a-skeleton>
</a-list-item>
</template>
</a-list>
<!-- 工单二维码 -->
<Qrcode v-model:visible="showQrcode" />
</a-card>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { pageTaskCount } from '@/api/oa/task-count';
import { TaskCount } from '@/api/oa/task-count/model';
import { UserAddOutlined } from '@ant-design/icons-vue';
import Qrcode from './qrcode.vue';
const showQrcode = ref(false);
const list = ref<TaskCount[]>([]);
pageTaskCount({ limit: 10, roleCode: 'commander' }).then((res) => {
if (res) {
list.value = res?.list;
}
});
const onShowQrcode = () => {
showQrcode.value = true;
};
</script>
<style lang="less" scoped>
.monitor-evaluate-text {
width: 90px;
flex-shrink: 0;
white-space: nowrap;
opacity: 0.8;
& > .anticon {
font-size: 12px;
margin: 0 6px 0 8px;
}
}
/deep/.ant-list-item {
padding: 7px 0;
}
</style>

View File

@@ -0,0 +1,70 @@
<!-- 本月目标 -->
<template>
<a-card :title="title" :bordered="false">
<template #extra>
<more-icon @remove="onRemove" @edit="onEdit" />
</template>
<div class="workplace-goal-group">
<a-progress
:width="180"
:percent="80"
type="dashboard"
:stroke-width="4"
:show-info="false"
/>
<div class="workplace-goal-content">
<ele-tag color="blue" size="large" shape="circle">
<trophy-outlined />
</ele-tag>
<div class="workplace-goal-num">285</div>
</div>
<div class="workplace-goal-text">恭喜, 本月目标已达标!</div>
</div>
</a-card>
</template>
<script lang="ts" setup>
import { TrophyOutlined } from '@ant-design/icons-vue';
import MoreIcon from './more-icon.vue';
defineProps<{
title?: string;
}>();
const emit = defineEmits<{
(e: 'remove'): void;
(e: 'edit'): void;
}>();
const onRemove = () => {
emit('remove');
};
const onEdit = () => {
emit('edit');
};
</script>
<style lang="less" scoped>
.workplace-goal-group {
height: 310px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
.workplace-goal-content {
position: absolute;
top: 50%;
left: 50%;
width: 180px;
margin: -50px 0 0 -90px;
text-align: center;
}
.workplace-goal-num {
font-size: 40px;
}
}
</style>

View File

@@ -0,0 +1,70 @@
<!-- 本月目标 -->
<template>
<a-card :title="title" :bordered="false">
<template #extra>
<more-icon @remove="onRemove" @edit="onEdit" />
</template>
<div class="workplace-goal-group">
<a-progress
:width="180"
:percent="80"
type="dashboard"
:stroke-width="4"
:show-info="false"
/>
<div class="workplace-goal-content">
<ele-tag color="blue" size="large" shape="circle">
<trophy-outlined />
</ele-tag>
<div class="workplace-goal-num">285</div>
</div>
<div class="workplace-goal-text">恭喜, 本月目标已达标!</div>
</div>
</a-card>
</template>
<script lang="ts" setup>
import { TrophyOutlined } from '@ant-design/icons-vue';
import MoreIcon from './more-icon.vue';
defineProps<{
title?: string;
}>();
const emit = defineEmits<{
(e: 'remove'): void;
(e: 'edit'): void;
}>();
const onRemove = () => {
emit('remove');
};
const onEdit = () => {
emit('edit');
};
</script>
<style lang="less" scoped>
.workplace-goal-group {
height: 310px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
.workplace-goal-content {
position: absolute;
top: 50%;
left: 50%;
width: 180px;
margin: -50px 0 0 -90px;
text-align: center;
}
.workplace-goal-num {
font-size: 40px;
}
}
</style>

View File

@@ -0,0 +1,178 @@
<!-- 快捷方式 -->
<template>
<a-row :gutter="16" ref="wrapRef">
<a-col v-for="item in data" :key="item.url" :lg="3" :md="6" :sm="9" :xs="8">
<a-card :bordered="false" hoverable :body-style="{ padding: 0 }">
<div class="app-link-block" @click="navTo(item)">
<component
:is="item.icon"
class="app-link-icon"
:style="{ color: item.color }"
/>
<div class="app-link-title">{{ item.title }}</div>
</div>
</a-card>
</a-col>
</a-row>
</template>
<script lang="ts" setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import SortableJs from 'sortablejs';
import type { Row as ARow } from 'ant-design-vue';
import { openUrl } from '@/utils/common';
import { getOriginDomain } from '@/utils/domain';
const CACHE_KEY = 'workplace-links';
// 当前开发环境
const env = import.meta.env.MODE;
interface LinkItem {
icon: string;
title: string;
url: string;
color?: string;
}
// 默认顺序
const DEFAULT: LinkItem[] = [
{
icon: 'settingOutlined',
title: '系统设置',
url: '/system/setting'
},
{
icon: 'AntDesignOutlined',
title: '项目管理',
url: '/oa/app/index'
},
{
icon: 'TeamOutlined',
title: '用户管理',
url: '/system/user'
},
{
icon: 'ShoppingOutlined',
title: '产品管理',
url: '/product/index'
},
{
icon: 'FileSearchOutlined',
title: '文章管理',
url: '/cms/article'
},
{
icon: 'ChromeOutlined',
title: '网址导航',
url: '/oa/link'
},
{
icon: 'AppstoreAddOutlined',
title: '扩展插件',
url: '/system/plug'
},
{
icon: 'DesktopOutlined',
title: '应用主页',
url: `http://${localStorage.getItem('TenantId')}.${localStorage.getItem(
'domain'
)}`
}
];
// 获取缓存的顺序
const cache = (() => {
const str = localStorage.getItem(CACHE_KEY);
try {
return str ? JSON.parse(str) : null;
} catch (e) {
return null;
}
})();
const data = ref<LinkItem[]>([...(cache ?? DEFAULT)]);
const wrapRef = ref<InstanceType<typeof ARow> | null>(null);
let sortableIns: SortableJs | null = null;
/* 重置布局 */
const reset = () => {
data.value = [...DEFAULT];
cacheData();
};
/* 缓存布局 */
const cacheData = () => {
localStorage.setItem(CACHE_KEY, JSON.stringify(data.value));
};
const navTo = (item) => {
if (item.icon == 'DesktopOutlined') {
if (env == 'development') {
return openUrl(getOriginDomain());
}
return openUrl(`http://www.${domain.value}`);
}
openUrl(item.url);
};
onMounted(() => {
const isTouchDevice = 'ontouchstart' in document.documentElement;
if (isTouchDevice) {
return;
}
sortableIns = new SortableJs(wrapRef.value?.$el, {
animation: 300,
onUpdate: ({ oldIndex, newIndex }) => {
if (typeof oldIndex === 'number' && typeof newIndex === 'number') {
const temp = [...data.value];
temp.splice(newIndex, 0, temp.splice(oldIndex, 1)[0]);
data.value = temp;
cacheData();
}
},
setData: () => {}
});
});
onBeforeUnmount(() => {
if (sortableIns) {
sortableIns.destroy();
}
});
defineExpose({ reset });
</script>
<script lang="ts">
import * as icons from './link-icons';
import { getSiteInfo } from '@/api/layout';
import { ref } from 'vue';
const tenantId = ref<number>();
const domain = ref<string>();
getSiteInfo().then((data) => {
tenantId.value = data.tenantId;
domain.value = data.domain;
});
export default {
components: icons
};
</script>
<style lang="less" scoped>
.app-link-block {
padding: 12px;
text-align: center;
display: block;
color: inherit;
.app-link-icon {
color: #666666;
font-size: 30px;
margin: 6px 0 10px 0;
}
}
</style>

View File

@@ -0,0 +1,12 @@
export {
UserOutlined,
TeamOutlined,
FileSearchOutlined,
ChromeOutlined,
ShoppingOutlined,
LaptopOutlined,
AppstoreAddOutlined,
DesktopOutlined,
AntDesignOutlined,
SettingOutlined
} from '@ant-design/icons-vue';

View File

@@ -0,0 +1,54 @@
<template>
<a-card :title="linkType" :bordered="false" :body-style="{ padding: '2px' }">
<template #extra
><a @click="openNew('/oa/link')" class="ele-text-placeholder"
>更多<RightOutlined /></a
></template>
<a-list :size="`small`" :split="false" :data-source="list">
<template #renderItem="{ item }">
<a-list-item>
<a-list-item-meta>
<template #avatar>
<a-avatar :src="item.icon" />
</template>
<template #title>
<a @click="openUrl(item.url)">{{ item.name }}</a>
</template>
</a-list-item-meta>
</a-list-item>
</template>
</a-list>
</a-card>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { openNew, openUrl } from '@/utils/common';
import { pageLink } from '@/api/oa/link';
import { RightOutlined } from '@ant-design/icons-vue';
const list = ref<any[]>([]);
const props = defineProps<{
linkType: string;
}>();
/**
* 加载数据
*/
const reload = () => {
const { linkType } = props;
// 加载文章列表
pageLink({ linkType, limit: 5 }).then((data) => {
if (data?.list) {
list.value = data.list;
}
});
};
reload();
</script>
<script lang="ts">
export default {
name: 'DashboardArticleList'
};
</script>

View File

@@ -0,0 +1,38 @@
<template>
<a-dropdown placement="bottomRight">
<more-outlined class="ele-text-secondary" style="font-size: 18px" />
<template #overlay>
<a-menu :selectable="false" @click="onClick">
<a-menu-item key="edit">
<div class="ele-cell">
<edit-outlined />
<div class="ele-cell-content">编辑</div>
</div>
</a-menu-item>
<a-menu-item key="remove">
<div class="ele-cell ele-text-danger">
<delete-outlined />
<div class="ele-cell-content">删除</div>
</div>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</template>
<script lang="ts" setup>
import {
MoreOutlined,
EditOutlined,
DeleteOutlined
} from '@ant-design/icons-vue';
const emit = defineEmits<{
(e: 'edit'): void;
(e: 'remove'): void;
}>();
const onClick = ({ key }) => {
emit(key);
};
</script>

View File

@@ -0,0 +1,121 @@
<!-- 用户信息 -->
<template>
<a-card :bordered="false" :body-style="{ padding: '20px' }">
<div class="ele-cell workplace-user-card">
<div class="ele-cell-content ele-cell">
<a-avatar :size="68" :src="loginUser.avatar">
<template v-if="!loginUser.avatar" #icon>
<user-outlined />
</template>
</a-avatar>
<div class="ele-cell-content">
<h4 class="ele-elip">
早安, {{ loginUser.nickname }}, 开始您一天的工作吧!
</h4>
<div class="ele-elip ele-text-secondary">
<cloud-outlined />
<em>{{ elip[Math.floor(Math.random() * elip.length)] }}</em>
<!-- <em>今日多云转阴18 - 22出门记得穿外套哦~</em>-->
</div>
</div>
</div>
<div class="workplace-count-group">
<!-- <div class="workplace-count-item">-->
<!-- <div class="workplace-count-header">-->
<!-- <ele-tag color="blue" shape="circle" size="small">-->
<!-- <appstore-filled />-->
<!-- </ele-tag>-->
<!-- <span class="workplace-count-name">项目数</span>-->
<!-- </div>-->
<!-- <h2>0</h2>-->
<!-- </div>-->
<!-- <div class="workplace-count-item">-->
<!-- <div class="workplace-count-header">-->
<!-- <ele-tag color="orange" shape="circle" size="small">-->
<!-- <check-square-outlined />-->
<!-- </ele-tag>-->
<!-- <span class="workplace-count-name">待办项</span>-->
<!-- </div>-->
<!-- <h2>6 / 24</h2>-->
<!-- </div>-->
<!-- <div class="workplace-count-item">-->
<!-- <div class="workplace-count-header">-->
<!-- <ele-tag color="green" shape="circle" size="small">-->
<!-- <bell-filled />-->
<!-- </ele-tag>-->
<!-- <span class="workplace-count-name">消息</span>-->
<!-- </div>-->
<!-- <h2>0</h2>-->
<!-- </div>-->
</div>
</div>
</a-card>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import {
UserOutlined,
CloudOutlined,
AppstoreFilled,
CheckSquareOutlined,
BellFilled
} from '@ant-design/icons-vue';
import { useUserStore } from '@/store/modules/user';
const userStore = useUserStore();
// 当前登录用户信息
const loginUser = computed(() => userStore.info ?? {});
const elip = ref<string[]>([
'小事成就大事,细节成就完美~',
'心态决定命运,自信走向成功',
'人生能有几回博,今日不博何时博',
'成功需要成本,时间也是一种成本,对时间的珍惜就是对成本的节约',
'有志者自有千方百计,无志者只感千难万难',
'积一时之跬步,臻千里之遥程'
]);
</script>
<style lang="less" scoped>
.workplace-user-card {
.ele-cell-content {
overflow: hidden;
}
h4 {
margin-bottom: 6px;
}
}
.workplace-count-group {
white-space: nowrap;
text-align: right;
flex-shrink: 0;
}
.workplace-count-item {
display: inline-block;
margin: 0 4px 0 24px;
}
.workplace-count-name {
margin-left: 8px;
}
@media screen and (max-width: 992px) {
.workplace-count-item {
margin: 0 2px 0 12px;
}
}
@media screen and (max-width: 768px) {
.workplace-user-card {
display: block;
}
.workplace-count-group {
margin-top: 8px;
}
}
</style>

View File

@@ -0,0 +1,66 @@
<template>
<ele-modal
:width="400"
:visible="visible"
:title="`邀请新成员`"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
:footer="null"
@ok="save"
>
<div
class="qrcode-list"
style="display: flex; justify-content: space-around"
>
<div>
<img :src="qrcode" width="240" height="240" />
<div
style="
display: flex;
justify-content: center;
font-size: 26px;
padding-top: 20px;
"
>使用微信扫一扫</div
>
</div>
</div>
</ele-modal>
</template>
<script lang="ts" setup>
import { User } from '@/api/system/user/model';
import { reactive, ref } from 'vue';
import { taskJoinQRCode } from '@/api/oa/task';
defineProps<{
// 弹窗是否打开
visible: boolean;
}>();
const emit = defineEmits<{
(e: 'done', user: User): void;
(e: 'update:visible', visible: boolean): void;
}>();
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
const qrcode = ref('');
// 用户信息
const form = reactive<User>({
userId: undefined,
nickname: undefined
});
const save = () => {
emit('done', form);
};
taskJoinQRCode({}).then((text) => {
qrcode.value = String(text);
});
</script>
<style scoped lang="less"></style>

View File

@@ -0,0 +1,112 @@
<template>
<a-card :title="title" :bordered="false" :body-style="{ padding: '2px' }">
<template #extra
><a @click="openUrl('/oa/task')" class="ele-text-placeholder"
>更多<RightOutlined /></a
></template>
<a-list :size="`small`" :split="false" :data-source="list">
<template #renderItem="{ item }">
<a-list-item>
<div class="app-box">
<div class="app-info">
<a
class="ele-text-secondary"
@click="openNew('/oa/task/detail/' + item.taskId)"
>
<a-typography-paragraph
ellipsis
:content="`【${item.taskType}】${item.name}`"
/>
</a>
</div>
<a class="ele-text-placeholder">
<a-tag v-if="item.progress === TOBEARRANGED" color="red"
>待安排</a-tag
>
<a-tag v-if="item.progress === PENDING" color="orange"
>待处理</a-tag
>
<a-tag v-if="item.progress === PROCESSING" color="purple"
>处理中</a-tag
>
<a-tag v-if="item.progress === TOBECONFIRMED" color="cyan"
>待评价</a-tag
>
<a-tag v-if="item.progress === COMPLETED" color="green"
>已完成</a-tag
>
<a-tag v-if="item.progress === CLOSED">已关闭</a-tag>
<div class="ele-text-danger" v-if="item.overdueDays">
已逾期{{ item.overdueDays }}
</div>
</a>
</div>
</a-list-item>
</template>
</a-list>
</a-card>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { openUrl } from '@/utils/common';
import { Task } from '@/api/oa/task/model';
import { pageTask } from '@/api/oa/task';
import { useUserStore } from '@/store/modules/user';
import {
CLOSED,
COMPLETED,
PENDING,
PROCESSING,
TOBEARRANGED,
TOBECONFIRMED
} from '@/api/oa/task/model/progress';
import { RightOutlined } from '@ant-design/icons-vue';
const props = defineProps<{
title: string;
}>();
const list = ref<Task[]>([]);
/**
* 加载数据
*/
const reload = () => {
const { title } = props;
const userStore = useUserStore();
const where = {
userId: undefined,
commander: userStore.info?.userId,
limit: 6,
status: 0
};
// 加载列表
pageTask(where).then((data) => {
if (data?.list) {
list.value = data.list;
}
});
};
reload();
</script>
<script lang="ts">
export default {
name: 'DashboardArticleList'
};
</script>
<style lang="less" scoped>
.app-box {
display: flex;
width: 100%;
justify-content: space-between;
overflow: hidden;
.app-info {
display: flex;
margin-left: 5px;
flex-direction: column;
width: 400px;
}
}
</style>

View File

@@ -0,0 +1,84 @@
<!-- 小组成员 -->
<template>
<a-card :title="title" :bordered="false" :body-style="{ padding: '2px 0px' }">
<template #extra>
<more-icon @remove="onRemove" @edit="onEdit" />
</template>
<div
v-for="(item, index) in userList"
:key="index"
class="ele-cell user-list-item"
>
<div style="flex-shrink: 0">
<a-avatar :size="46" :src="item.avatar" />
</div>
<div class="ele-cell-content">
<span class="ele-cell-title ele-elip">{{ item.nickname }}</span>
<div class="ele-cell-desc ele-elip">{{ item.phone }}</div>
</div>
<div style="flex-shrink: 0">
<a-tag :color="['green', 'red'][item.status]">
{{ ['在线', '离线'][item.status] }}
</a-tag>
</div>
</div>
</a-card>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import MoreIcon from './more-icon.vue';
import { pageUsers } from '@/api/system/user';
import type { User } from '@/api/system/user/model';
defineProps<{
title?: string;
}>();
const emit = defineEmits<{
(e: 'remove'): void;
(e: 'edit'): void;
}>();
// 小组成员数据
const userList = ref<User[]>([]);
/* 查询小组成员 */
const queryUserList = () => {
pageUsers({ parentId: 11, limit: 5 }).then((data: any) => {
userList.value = data.list;
});
};
const onRemove = () => {
emit('remove');
};
const onEdit = () => {
emit('edit');
};
queryUserList();
</script>
<style lang="less" scoped>
.user-list-item {
padding: 12px 18px;
& + .user-list-item {
border-top: 1px solid hsla(0, 0%, 60%, 0.15);
}
.ele-cell-content {
overflow: hidden;
}
.ele-cell-desc {
margin-top: 0;
}
.ant-tag {
margin: 0;
}
}
</style>

View File

@@ -0,0 +1,359 @@
<template>
<div class="ele-body ele-body-card">
<a-card :bordered="false">
<template #title>
<SoundOutlined class="ele-text-danger" />
<span class="gg-title ele-text-heading">公告</span>
<a @click="openNew('/cms/article/374')" class="ele-text-heading"
>系统内测中如何获取体验账号</a
>
</template>
<template #extra>
<a @click="openNew('/cms/category/92')" class="ele-text-placeholder"
>更多<RightOutlined
/></a>
</template>
</a-card>
<!-- <profile-card />-->
<LinkCard />
<a-row :gutter="16" ref="wrapRef">
<a-col :md="9">
<App title="我的项目" />
</a-col>
<a-col :md="9">
<Task title="我的工单" :categoryId="92" />
</a-col>
<a-col :md="6">
<Article title="通知公告" :categoryId="92" />
</a-col>
<a-col :md="6">
<Article title="公司动态" :categoryId="37" />
</a-col>
<a-col :md="6">
<Article title="经验分享" :categoryId="93" />
</a-col>
<a-col :md="6">
<Article title="API文档" :categoryId="90" />
</a-col>
<a-col :md="6">
<Link linkType="实用工具" />
</a-col>
<!-- <a-col :md="12">-->
<!-- <a-card title="商品管理" :bordered="false" />-->
<!-- </a-col>-->
</a-row>
<!-- <a-row :gutter="16" ref="wrapRef">-->
<!-- <a-col-->
<!-- v-for="(item, index) in data"-->
<!-- :key="item.name"-->
<!-- :lg="item.lg"-->
<!-- :md="item.md"-->
<!-- :sm="item.sm"-->
<!-- :xs="item.xs"-->
<!-- >-->
<!-- <component-->
<!-- :is="item.name"-->
<!-- :title="item.title"-->
<!-- @remove="onRemove(index)"-->
<!-- @edit="onEdit(index)"-->
<!-- />-->
<!-- </a-col>-->
<!-- </a-row>-->
<a-card :bordered="false" :body-style="{ padding: 0 }">
<div class="ele-cell" style="line-height: 42px">
<div
class="ele-cell-content ele-text-primary workplace-bottom-btn"
@click="add"
>
<PlusCircleOutlined /> 添加视图
</div>
<a-divider type="vertical" />
<div
class="ele-cell-content ele-text-primary workplace-bottom-btn"
@click="reset"
>
<UndoOutlined /> 重置布局
</div>
</div>
</a-card>
<ele-modal
:width="680"
v-model:visible="visible"
title="未添加的视图"
:footer="null"
>
<a-row :gutter="16">
<a-col
v-for="item in notAddedData"
:key="item.name"
:md="8"
:sm="12"
:xs="24"
>
<div
class="workplace-card-item ele-border-split"
@click="addView(item)"
>
<div class="workplace-card-header ele-border-split">
{{ item.title }}
</div>
<div class="workplace-card-body ele-text-placeholder">
<plus-circle-outlined />
</div>
</div>
</a-col>
</a-row>
<a-empty v-if="!notAddedData.length" description="已添加所有视图" />
</ele-modal>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
import SortableJs from 'sortablejs';
import type { Row as ARow } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import Article from './components/article-list.vue';
import Link from './components/link-list.vue';
import App from './components/app-list.vue';
import Task from './components/task-card.vue';
import {
PlusCircleOutlined,
SoundOutlined,
RightOutlined
} from '@ant-design/icons-vue';
import { openNew } from '@/utils/common';
const CACHE_KEY = 'workplace-layout';
interface ViewItem {
name: string;
title: string;
lg: number;
md: number;
sm: number;
xs: number;
}
// 默认布局
const DEFAULT: ViewItem[] = [
{
name: 'link',
title: '网址导航',
lg: 24,
md: 24,
sm: 24,
xs: 24
},
{
name: 'task-card',
title: '我的工单',
lg: 18,
md: 24,
sm: 24,
xs: 24
},
// {
// name: 'project-card',
// title: '项目管理',
// lg: 16,
// md: 24,
// sm: 24,
// xs: 24
// },
{
name: 'user-list',
title: '小组成员',
lg: 6,
md: 24,
sm: 24,
xs: 24
}
// {
// name: 'activities-card',
// title: '最新动态',
// lg: 6,
// md: 24,
// sm: 24,
// xs: 24
// },
// {
// name: 'goal-card',
// title: '本月目标',
// lg: 8,
// md: 24,
// sm: 24,
// xs: 24
// },
// {
// name: 'docs',
// title: '知识库',
// lg: 8,
// md: 24,
// sm: 24,
// xs: 24
// }
];
// 获取缓存的顺序
const cache = (() => {
const str = localStorage.getItem(CACHE_KEY);
try {
return str ? JSON.parse(str) : null;
} catch (e) {
return null;
}
})();
const data = ref<ViewItem[]>([...(cache ?? DEFAULT)]);
const visible = ref(false);
const wrapRef = ref<InstanceType<typeof ARow> | null>(null);
let sortableIns: SortableJs | null = null;
// 未添加的数据
const notAddedData = computed(() => {
return DEFAULT.filter((d) => !data.value.some((t) => t.name === d.name));
});
/* 添加 */
const add = () => {
visible.value = true;
};
/* 重置布局 */
const reset = () => {
data.value = [...DEFAULT];
cacheData();
message.success('已重置');
};
/* 缓存布局 */
const cacheData = () => {
localStorage.setItem(CACHE_KEY, JSON.stringify(data.value));
};
/* 删除视图 */
const onRemove = (index: number) => {
data.value = data.value.filter((_d, i) => i !== index);
cacheData();
};
/* 编辑视图 */
const onEdit = (index: number) => {
data.value.map((d) => {
if (d.name == 'user-list') {
}
});
// message.info('点击了编辑');
};
/* 添加视图 */
const addView = (item) => {
data.value.push(item);
cacheData();
message.success('已添加');
};
onMounted(() => {
const isTouchDevice = 'ontouchstart' in document.documentElement;
if (isTouchDevice) {
return;
}
sortableIns = new SortableJs(wrapRef.value?.$el, {
handle: '.ant-card-head',
animation: 300,
onUpdate: ({ oldIndex, newIndex }) => {
if (typeof oldIndex === 'number' && typeof newIndex === 'number') {
const temp = [...data.value];
temp.splice(newIndex, 0, temp.splice(oldIndex, 1)[0]);
data.value = temp;
cacheData();
}
},
setData: () => {}
});
});
onBeforeUnmount(() => {
if (sortableIns) {
sortableIns.destroy();
}
});
</script>
<script lang="ts">
import ActivitiesCard from './components/activities-card.vue';
import TaskCard from './components/task-card.vue';
import GoalCard from './components/goal-card.vue';
import UserList from './components/count-user.vue';
import Docs from './components/docs.vue';
import LinkCard from './components/link-card.vue';
import ProfileCard from './components/profile-card.vue';
export default {
name: 'DashboardWorkplace',
components: {
LinkCard,
UserList,
ActivitiesCard,
TaskCard,
GoalCard,
Docs,
ProfileCard
}
};
</script>
<style lang="less" scoped>
.ele-body :deep(.ant-card-head) {
cursor: move;
position: relative;
}
.ele-body :deep(.ant-row > .ant-col.sortable-chosen > .ant-card) {
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.2);
}
.workplace-bottom-btn {
text-align: center;
cursor: pointer;
transition: background-color 0.2s;
}
.workplace-bottom-btn:hover {
background: hsla(0, 0%, 60%, 0.05);
}
/* 添加弹窗 */
.workplace-card-item {
margin-bottom: 15px;
border-width: 1px;
border-style: solid;
border-radius: 4px;
position: relative;
cursor: pointer;
transition: box-shadow 0.2s, background-color 0.2s;
}
.workplace-card-item:hover {
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.1);
background: hsla(0, 0%, 60%, 0.05);
}
.workplace-card-item .workplace-card-header {
border-bottom-width: 1px;
border-bottom-style: solid;
padding: 8px;
}
.gg-title {
padding: 0 5px;
margin-right: 20px;
}
.workplace-card-body {
font-size: 26px;
padding: 24px 10px;
text-align: center;
}
</style>

View File

@@ -0,0 +1,23 @@
<script setup lang="ts">
import { ref } from 'vue';
const top = ref<number>(10);
const bottom = ref<number>(10);
const change = (affixed: boolean) => {
console.log(affixed);
};
</script>
<template>
<div class="ele-body">
<a-card title="测试页面" style="height: 2000px">
收到佛山市
<a-affix :offset-top="120" @change="change">
<a-button>120px to affix top</a-button>
</a-affix>
</a-card>
</div>
</template>
<style scoped lang="less"></style>

View File

@@ -0,0 +1,165 @@
<!-- 字典项编辑弹窗 -->
<template>
<ele-modal
:width="460"
:visible="visible"
:confirm-loading="loading"
:body-style="{ paddingBottom: '8px' }"
:title="isUpdate ? '修改字典项' : '添加字典项'"
@update:visible="updateVisible"
@ok="save"
>
<a-form
:label-col="{ md: { span: 6 }, sm: { span: 6 }, xs: { span: 24 } }"
:wrapper-col="{ md: { span: 18 }, sm: { span: 18 }, xs: { span: 24 } }"
>
<a-form-item label="字典项名称" v-bind="validateInfos.dictDataName">
<a-input
allow-clear
:maxlength="100"
placeholder="请输入字典项名称"
v-model:value="form.dictDataName"
/>
</a-form-item>
<a-form-item label="字典项值" v-bind="validateInfos.dictDataCode">
<a-input
allow-clear
:maxlength="100"
placeholder="请输入字典项值"
v-model:value="form.dictDataCode"
/>
</a-form-item>
<a-form-item label="排序号" v-bind="validateInfos.sortNumber">
<a-input-number
:min="0"
:max="9999"
class="ele-fluid"
placeholder="请输入排序号"
v-model:value="form.sortNumber"
/>
</a-form-item>
<a-form-item label="备注">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入备注"
v-model:value="form.comments"
/>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject } from 'ele-admin-pro';
import { addDictData, updateDictData } from '@/api/system/dict-data';
import type { DictData } from '@/api/system/dict-data/model';
const useForm = Form.useForm;
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: DictData | null;
// 字典id
dictId: number;
}>();
// 是否是修改
const isUpdate = ref(false);
// 提交状态
const loading = ref(false);
// 表单数据
const form = reactive<DictData>({
dictDataId: undefined,
dictDataName: '',
dictDataCode: '',
sortNumber: 100,
comments: ''
});
// 表单验证规则
const rules = reactive({
dictDataName: [
{
required: true,
message: '请输入字典项名称',
type: 'string',
trigger: 'blur'
}
],
dictDataCode: [
{
required: true,
message: '请输入字典项值',
type: 'string',
trigger: 'blur'
}
],
sortNumber: [
{
required: true,
message: '请输入排序号',
type: 'number',
trigger: 'blur'
}
]
});
const { resetFields, validate, validateInfos } = useForm(form, rules);
/* 保存编辑 */
const save = () => {
validate()
.then(() => {
loading.value = true;
const saveOrUpdate = isUpdate.value ? updateDictData : addDictData;
saveOrUpdate({
...form,
dictId: props.dictId
})
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
/* 更新visible */
const updateVisible = (value) => {
emit('update:visible', value);
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
assignObject(form, props.data);
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
}
);
</script>

View File

@@ -0,0 +1,70 @@
<!-- 搜索表单 -->
<template>
<a-row :gutter="16">
<a-col :xl="6" :lg="8" :md="11" :sm="24" :xs="24">
<a-input
v-model:value.trim="where.keywords"
placeholder="输入关键字搜索"
allow-clear
/>
</a-col>
<a-col :xl="18" :lg="16" :md="13" :sm="24" :xs="24">
<a-space :size="10" style="flex-wrap: wrap">
<a-button type="primary" class="ele-btn-icon" @click="search">
<template #icon>
<SearchOutlined />
</template>
<span>查询</span>
</a-button>
<a-button type="primary" class="ele-btn-icon" @click="add">
<template #icon>
<PlusOutlined />
</template>
<span>新建</span>
</a-button>
<a-button danger type="primary" class="ele-btn-icon" @click="remove">
<template #icon>
<DeleteOutlined />
</template>
<span>删除</span>
</a-button>
</a-space>
</a-col>
</a-row>
</template>
<script lang="ts" setup>
import {
PlusOutlined,
DeleteOutlined,
SearchOutlined
} from '@ant-design/icons-vue';
import useSearch from '@/utils/use-search';
import type { DictDataParam } from '@/api/system/dict-data/model';
const emit = defineEmits<{
(e: 'search', where?: DictDataParam): void;
(e: 'add'): void;
(e: 'remove'): void;
}>();
// 表单数据
const { where } = useSearch<DictDataParam>({
keywords: ''
});
/* 搜索 */
const search = () => {
emit('search', where);
};
/* 添加 */
const add = () => {
emit('add');
};
/* 删除 */
const remove = () => {
emit('remove');
};
</script>

View File

@@ -0,0 +1,229 @@
<template>
<!-- 表格 -->
<ele-pro-table
ref="tableRef"
row-key="dictDataId"
:columns="columns"
:datasource="datasource"
tool-class="ele-toolbar-form"
v-model:selection="selection"
:customRow="customRow"
:row-selection="{ columnWidth: 48 }"
:scroll="{ x: 800 }"
height="calc(100vh - 290px)"
tools-theme="default"
bordered
class="sys-dict-data-table"
>
<template #toolbar>
<dict-data-search
@search="reload"
@add="openEdit()"
@remove="removeBatch"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'comments'">
<span class="ele-text-placeholder">{{ record.comments }}</span>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical" />
<a-popconfirm
title="确定要删除此字典项吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
<!-- 编辑弹窗 -->
<dict-data-edit
v-model:visible="showEdit"
:data="current"
:dict-id="dictId"
@done="reload"
/>
</template>
<script lang="ts" setup>
import { createVNode, ref, watch } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import { toDateString } from 'ele-admin-pro';
import DictDataSearch from './dict-data-search.vue';
import DictDataEdit from './dict-data-edit.vue';
import {
pageDictData,
removeDictData,
removeDictDataBatch
} from '@/api/system/dict-data';
import type { DictData, DictDataParam } from '@/api/system/dict-data/model';
import { Dict } from "@/api/system/dict/model";
const props = defineProps<{
// 字典id
dictId: number;
}>();
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '字典项名称',
dataIndex: 'dictDataName',
ellipsis: true,
sorter: true
},
{
title: '字典项值',
dataIndex: 'dictDataCode',
ellipsis: true,
sorter: true
},
{
title: '备注',
dataIndex: 'comments',
key: 'comments',
sorter: true
},
{
title: '排序号',
dataIndex: 'sortNumber',
sorter: true,
width: 120,
align: 'center'
},
// {
// title: '创建时间',
// dataIndex: 'createTime',
// sorter: true,
// ellipsis: true,
// customRender: ({ text }) => toDateString(text)
// },
{
title: '操作',
key: 'action',
width: 130,
align: 'center',
hideInSetting: true
}
]);
// 表格选中数据
const selection = ref<DictData[]>([]);
// 当前编辑数据
const current = ref<DictData | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 表格数据源
const datasource: DatasourceFunction = ({ page, limit, where, orders }) => {
return pageDictData({
...where,
...orders,
page,
limit,
dictId: props.dictId
});
};
/* 刷新表格 */
const reload = (where?: DictDataParam) => {
tableRef?.value?.reload({ page: 1, where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: DictData) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 删除单个 */
const remove = (row: DictData) => {
const hide = message.loading('请求中..', 0);
removeDictData(row.dictDataId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的字典项吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeDictDataBatch(selection.value.map((d) => d.dictDataId))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 自定义行属性 */
const customRow = (record: Dict) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
// 监听字典id变化
watch(
() => props.dictId,
() => {
reload();
}
);
</script>
<style lang="less" scoped>
.sys-dict-data-table :deep(.ant-table-body) {
overflow: auto !important;
overflow: overlay !important;
}
.sys-dict-data-table :deep(.ant-table-pagination.ant-pagination) {
padding: 0 4px;
margin-bottom: 0;
}
</style>

View File

@@ -0,0 +1,160 @@
<!-- 字典编辑弹窗 -->
<template>
<ele-modal
:width="460"
:visible="visible"
:confirm-loading="loading"
:title="isUpdate ? '修改字典' : '添加字典'"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
:label-col="{ md: { span: 5 }, sm: { span: 5 }, xs: { span: 24 } }"
:wrapper-col="{ md: { span: 19 }, sm: { span: 19 }, xs: { span: 24 } }"
>
<a-form-item label="字典名称" v-bind="validateInfos.dictName">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入字典名称"
v-model:value="form.dictName"
/>
</a-form-item>
<a-form-item label="字典标识" v-bind="validateInfos.dictCode">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入字典标识"
v-model:value="form.dictCode"
/>
</a-form-item>
<a-form-item label="排序号" v-bind="validateInfos.sortNumber">
<a-input-number
:min="0"
:max="9999"
class="ele-fluid"
placeholder="请输入排序号"
v-model:value="form.sortNumber"
/>
</a-form-item>
<a-form-item label="备注">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入备注"
v-model:value="form.comments"
/>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject } from 'ele-admin-pro';
import { addDict, updateDict } from '@/api/system/dict';
import type { Dict } from '@/api/system/dict/model';
const useForm = Form.useForm;
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Dict | null;
}>();
// 是否是修改
const isUpdate = ref(false);
// 提交状态
const loading = ref(false);
// 表单数据
const form = reactive<Dict>({
dictId: undefined,
dictName: '',
dictCode: '',
sortNumber: 100,
comments: ''
});
// 表单验证规则
const rules = reactive({
dictName: [
{
required: true,
message: '请输入字典名称',
type: 'string',
trigger: 'blur'
}
],
dictCode: [
{
required: true,
message: '请输入字典标识',
type: 'string',
trigger: 'blur'
}
],
sortNumber: [
{
required: true,
message: '请输入排序号',
type: 'number',
trigger: 'blur'
}
]
});
const { resetFields, validate, validateInfos } = useForm(form, rules);
/* 保存编辑 */
const save = () => {
validate()
.then(() => {
loading.value = true;
const saveOrUpdate = isUpdate.value ? updateDict : addDict;
saveOrUpdate(form)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
assignObject(form, props.data);
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
}
);
</script>

View File

@@ -0,0 +1,192 @@
<template>
<div class="ele-body">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-split-layout
width="266px"
allow-collapse
:right-style="{ overflow: 'hidden' }"
:style="{ minHeight: 'calc(100vh - 152px)' }"
>
<!-- 表格 -->
<ele-pro-table
ref="tableRef"
row-key="dictId"
:columns="columns"
:datasource="datasource"
v-model:current="current"
selection-type="radio"
:row-selection="{ columnWidth: 32 }"
:need-page="false"
:toolkit="[]"
height="calc(100vh - 290px)"
tools-theme="default"
class="sys-dict-table"
@done="done"
>
<template #toolbar>
<a-space :size="10">
<a-button type="primary" class="ele-btn-icon" @click="openEdit()">
<template #icon>
<plus-outlined />
</template>
<span>新建</span>
</a-button>
<a-button
type="primary"
:disabled="!current"
class="ele-btn-icon"
@click="openEdit(current)"
>
<template #icon>
<edit-outlined />
</template>
<span>修改</span>
</a-button>
<a-button
danger
type="primary"
:disabled="!current"
class="ele-btn-icon"
@click="remove"
>
<template #icon>
<delete-outlined />
</template>
<span>删除</span>
</a-button>
</a-space>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'dictName'">
{{ record.dictName }}
<span class="ele-text-placeholder">
{{ record.dictCode }}
</span>
</template>
</template>
</ele-pro-table>
<template #content>
<dict-data
v-if="current && current.dictId"
:dict-id="current.dictId"
/>
</template>
</ele-split-layout>
</a-card>
<!-- 编辑弹窗 -->
<dict-edit v-model:visible="showEdit" :data="editData" @done="reload" />
</div>
</template>
<script lang="ts" setup>
import { createVNode, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
ExclamationCircleOutlined
} from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem,
EleProTableDone
} from 'ele-admin-pro/es/ele-pro-table/types';
import DictData from './components/dict-data.vue';
import DictEdit from './components/dict-edit.vue';
import { listDictionaries, removeDict } from '@/api/system/dict';
import type { Dict } from '@/api/system/dict/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格列配置
const columns = ref<ColumnItem[]>([
{
key: 'index',
width: 35,
ellipsis: true,
align: 'center',
fixed: 'left',
hideInSetting: true,
customRender: ({ index }) => index + (tableRef.value?.tableIndex ?? 0)
},
{
title: '字典名称',
dataIndex: 'dictName',
key: 'dictName'
}
]);
// 表格选中数据
const current = ref<Dict | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 编辑回显数据
const editData = ref<Dict | null>(null);
// 表格数据源
const datasource: DatasourceFunction = () => {
return listDictionaries();
};
/* 表格渲染完成回调 */
const done: EleProTableDone<Dict> = (res) => {
if (res.data?.length) {
current.value = res.data[0];
}
};
/* 刷新表格 */
const reload = () => {
tableRef?.value?.reload();
};
/* 打开编辑弹窗 */
const openEdit = (row?: Dict | null) => {
editData.value = row ?? null;
showEdit.value = true;
};
/* 删除 */
const remove = () => {
Modal.confirm({
title: '提示',
content: '确定要删除选中的字典吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeDict(current.value?.dictId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
</script>
<script lang="ts">
export default {
name: 'SystemDict'
};
</script>
<style lang="less" scoped>
.sys-dict-table :deep(.ant-table-body) {
overflow: auto !important;
overflow: overlay !important;
}
.dict-code {
color: #cccccc;
}
</style>

View File

@@ -0,0 +1,210 @@
<template>
<div class="ele-body">
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<ele-split-layout
width="266px"
allow-collapse
:right-style="{ overflow: 'hidden' }"
:style="{ minHeight: 'calc(100vh - 152px)' }"
>
<!-- 表格 -->
<ele-pro-table
ref="tableRef"
row-key="dictId"
:columns="columns"
:datasource="datasource"
v-model:current="current"
:customRow="customRow"
selection-type="radio"
:row-selection="{ columnWidth: 32 }"
:need-page="false"
:toolkit="[]"
height="calc(100vh - 290px)"
tools-theme="default"
bordered
class="sys-dict-table"
@done="done"
>
<template #toolbar>
<a-space :size="10">
<a-button type="primary" class="ele-btn-icon" @click="openEdit()">
<template #icon>
<plus-outlined />
</template>
<span>新建</span>
</a-button>
<a-button
type="primary"
:disabled="!current"
class="ele-btn-icon"
@click="openEdit(current)"
>
<template #icon>
<edit-outlined />
</template>
<span>修改</span>
</a-button>
<a-button
danger
type="primary"
:disabled="!current"
class="ele-btn-icon"
@click="remove"
>
<template #icon>
<delete-outlined />
</template>
<span>删除</span>
</a-button>
</a-space>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'dictName'">
<a-space :size="size">
<a-tooltip :title="`${record.dictCode}`">
{{ record.dictName }}
<!-- <span class="dict-code">{{ record.dictCode }}</span>-->
</a-tooltip>
</a-space>
</template>
</template>
</ele-pro-table>
<template #content>
<dict-data
v-if="current && current.dictId"
:dict-id="current.dictId"
/>
</template>
</ele-split-layout>
</a-card>
<!-- 编辑弹窗 -->
<dict-edit v-model:visible="showEdit" :data="editData" @done="reload" />
</div>
</template>
<script lang="ts" setup>
import { createVNode, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
ExclamationCircleOutlined
} from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem,
EleProTableDone
} from 'ele-admin-pro/es/ele-pro-table/types';
import DictData from './components/dict-data.vue';
import DictEdit from './components/dict-edit.vue';
import { listDictionaries, removeDict } from '@/api/system/dict';
import type { Dict } from '@/api/system/dict/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格列配置
const columns = ref<ColumnItem[]>([
{
key: 'index',
width: 45,
ellipsis: true,
align: 'center',
fixed: 'left',
hideInSetting: true,
customRender: ({ index }) => index + (tableRef.value?.tableIndex ?? 0)
},
{
title: '字典名称',
dataIndex: 'dictName',
key: 'dictName'
}
]);
// 表格选中数据
const current = ref<Dict | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 编辑回显数据
const editData = ref<Dict | null>(null);
// 表格数据源
const datasource: DatasourceFunction = () => {
return listDictionaries();
};
/* 表格渲染完成回调 */
const done: EleProTableDone<Dict> = (res) => {
if (res.data?.length) {
current.value = res.data[0];
}
};
/* 刷新表格 */
const reload = () => {
tableRef?.value?.reload();
};
/* 打开编辑弹窗 */
const openEdit = (row?: Dict | null) => {
editData.value = row ?? null;
showEdit.value = true;
};
/* 删除 */
const remove = () => {
Modal.confirm({
title: '提示',
content: '确定要删除选中的字典吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeDict(current.value?.dictId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 自定义行属性 */
const customRow = (record: Dict) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
</script>
<script lang="ts">
export default {
name: 'SystemDict'
};
</script>
<style lang="less" scoped>
.sys-dict-table :deep(.ant-table-body) {
overflow: auto !important;
overflow: overlay !important;
}
.dict-code {
color: #cccccc;
}
</style>

View File

@@ -0,0 +1,170 @@
<!-- 字典项编辑弹窗 -->
<template>
<ele-modal
:width="460"
:visible="visible"
:confirm-loading="loading"
:body-style="{ paddingBottom: '8px' }"
:title="isUpdate ? '修改字典项' : '添加字典项'"
@update:visible="updateVisible"
@ok="save"
>
<a-form
:label-col="{ md: { span: 6 }, sm: { span: 6 }, xs: { span: 24 } }"
:wrapper-col="{ md: { span: 18 }, sm: { span: 18 }, xs: { span: 24 } }"
>
<a-form-item label="字典项名称" v-bind="validateInfos.dictDataName">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入字典项名称"
v-model:value="form.dictDataName"
/>
</a-form-item>
<a-form-item label="字典项值" v-bind="validateInfos.dictDataCode">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入字典项值"
v-model:value="form.dictDataCode"
/>
</a-form-item>
<a-form-item label="排序号" v-bind="validateInfos.sortNumber">
<a-input-number
:min="0"
:max="9999"
class="ele-fluid"
placeholder="请输入排序号"
v-model:value="form.sortNumber"
/>
</a-form-item>
<a-form-item label="备注">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入备注"
v-model:value="form.comments"
/>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject } from 'ele-admin-pro';
import {
addDictionaryData,
updateDictionaryData
} from '@/api/system/dictionary-data';
import type { DictionaryData } from '@/api/system/dictionary-data/model';
const useForm = Form.useForm;
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: DictionaryData | null;
// 字典id
dictId: number;
}>();
// 是否是修改
const isUpdate = ref(false);
// 提交状态
const loading = ref(false);
// 表单数据
const form = reactive<DictionaryData>({
dictDataId: undefined,
dictDataName: '',
dictDataCode: '',
sortNumber: 100,
comments: ''
});
// 表单验证规则
const rules = reactive({
dictDataName: [
{
required: true,
message: '请输入字典项名称',
type: 'string',
trigger: 'blur'
}
],
dictDataCode: [
{
required: true,
message: '请输入字典项值',
type: 'string',
trigger: 'blur'
}
],
sortNumber: [
{
required: true,
message: '请输入排序号',
type: 'number',
trigger: 'blur'
}
]
});
const { resetFields, validate, validateInfos } = useForm(form, rules);
/* 保存编辑 */
const save = () => {
validate()
.then(() => {
loading.value = true;
const saveOrUpdate = isUpdate.value
? updateDictionaryData
: addDictionaryData;
saveOrUpdate({
...form,
dictId: props.dictId
})
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
/* 更新visible */
const updateVisible = (value) => {
emit('update:visible', value);
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
assignObject(form, props.data);
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
}
);
</script>

View File

@@ -0,0 +1,70 @@
<!-- 搜索表单 -->
<template>
<a-row :gutter="16">
<a-col :xl="6" :lg="8" :md="11" :sm="24" :xs="24">
<a-input
v-model:value.trim="where.keywords"
placeholder="输入关键字搜索"
allow-clear
/>
</a-col>
<a-col :xl="18" :lg="16" :md="13" :sm="24" :xs="24">
<a-space :size="10" style="flex-wrap: wrap">
<a-button type="primary" class="ele-btn-icon" @click="search">
<template #icon>
<search-outlined />
</template>
<span>查询</span>
</a-button>
<a-button type="primary" class="ele-btn-icon" @click="add">
<template #icon>
<plus-outlined />
</template>
<span>新建</span>
</a-button>
<a-button danger type="primary" class="ele-btn-icon" @click="remove">
<template #icon>
<delete-outlined />
</template>
<span>删除</span>
</a-button>
</a-space>
</a-col>
</a-row>
</template>
<script lang="ts" setup>
import {
PlusOutlined,
DeleteOutlined,
SearchOutlined
} from '@ant-design/icons-vue';
import useSearch from '@/utils/use-search';
import type { DictionaryDataParam } from '@/api/system/dictionary-data/model';
const emit = defineEmits<{
(e: 'search', where?: DictionaryDataParam): void;
(e: 'add'): void;
(e: 'remove'): void;
}>();
// 表单数据
const { where } = useSearch<DictionaryDataParam>({
keywords: ''
});
/* 搜索 */
const search = () => {
emit('search', where);
};
/* 添加 */
const add = () => {
emit('add');
};
/* 删除 */
const remove = () => {
emit('remove');
};
</script>

View File

@@ -0,0 +1,207 @@
<template>
<!-- 表格 -->
<ele-pro-table
ref="tableRef"
row-key="dictDataId"
:columns="columns"
:datasource="datasource"
tool-class="ele-toolbar-form"
v-model:selection="selection"
:row-selection="{ columnWidth: 48 }"
:scroll="{ x: 800 }"
height="calc(100vh - 290px)"
tools-theme="default"
bordered
class="sys-dict-data-table"
>
<template #toolbar>
<dict-data-search
@search="reload"
@add="openEdit()"
@remove="removeBatch"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical" />
<a-popconfirm
title="确定要删除此字典项吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
<!-- 编辑弹窗 -->
<dict-data-edit
v-model:visible="showEdit"
:data="current"
:dict-id="dictId"
@done="reload"
/>
</template>
<script lang="ts" setup>
import { createVNode, ref, watch } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import { toDateString } from 'ele-admin-pro';
import DictDataSearch from './dict-data-search.vue';
import DictDataEdit from './dict-data-edit.vue';
import {
pageDictionaryData,
removeDictionaryData,
removeDictionaryDataBatch
} from '@/api/system/dictionary-data';
import type {
DictionaryData,
DictionaryDataParam
} from '@/api/system/dictionary-data/model';
const props = defineProps<{
// 字典id
dictId: number;
}>();
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '字典项名称',
dataIndex: 'dictDataName',
ellipsis: true,
sorter: true
},
{
title: '字典项值',
dataIndex: 'dictDataCode',
ellipsis: true,
sorter: true
},
{
title: '排序号',
dataIndex: 'sortNumber',
sorter: true,
width: 120,
align: 'center'
},
{
title: '创建时间',
dataIndex: 'createTime',
sorter: true,
ellipsis: true,
customRender: ({ text }) => toDateString(text)
},
{
title: '操作',
key: 'action',
width: 130,
align: 'center',
hideInSetting: true
}
]);
// 表格选中数据
const selection = ref<DictionaryData[]>([]);
// 当前编辑数据
const current = ref<DictionaryData | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 表格数据源
const datasource: DatasourceFunction = ({ page, limit, where, orders }) => {
return pageDictionaryData({
...where,
...orders,
page,
limit,
dictId: props.dictId
});
};
/* 刷新表格 */
const reload = (where?: DictionaryDataParam) => {
tableRef?.value?.reload({ page: 1, where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: DictionaryData) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 删除单个 */
const remove = (row: DictionaryData) => {
const hide = message.loading('请求中..', 0);
removeDictionaryData(row.dictDataId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的字典项吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeDictionaryDataBatch(selection.value.map((d) => d.dictDataId))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
// 监听字典id变化
watch(
() => props.dictId,
() => {
reload();
}
);
</script>
<style lang="less" scoped>
.sys-dict-data-table :deep(.ant-table-body) {
overflow: auto !important;
overflow: overlay !important;
}
.sys-dict-data-table :deep(.ant-table-pagination.ant-pagination) {
padding: 0 4px;
margin-bottom: 0;
}
</style>

View File

@@ -0,0 +1,160 @@
<!-- 字典编辑弹窗 -->
<template>
<ele-modal
:width="460"
:visible="visible"
:confirm-loading="loading"
:title="isUpdate ? '修改字典' : '添加字典'"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
:label-col="{ md: { span: 5 }, sm: { span: 5 }, xs: { span: 24 } }"
:wrapper-col="{ md: { span: 19 }, sm: { span: 19 }, xs: { span: 24 } }"
>
<a-form-item label="字典名称" v-bind="validateInfos.dictName">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入字典名称"
v-model:value="form.dictName"
/>
</a-form-item>
<a-form-item label="字典标识" v-bind="validateInfos.dictCode">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入字典标识"
v-model:value="form.dictCode"
/>
</a-form-item>
<a-form-item label="排序号" v-bind="validateInfos.sortNumber">
<a-input-number
:min="0"
:max="9999"
class="ele-fluid"
placeholder="请输入排序号"
v-model:value="form.sortNumber"
/>
</a-form-item>
<a-form-item label="备注">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入备注"
v-model:value="form.comments"
/>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject } from 'ele-admin-pro';
import { addDictionary, updateDictionary } from '@/api/system/dictionary';
import type { Dictionary } from '@/api/system/dictionary/model';
const useForm = Form.useForm;
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Dictionary | null;
}>();
// 是否是修改
const isUpdate = ref(false);
// 提交状态
const loading = ref(false);
// 表单数据
const form = reactive<Dictionary>({
dictId: undefined,
dictName: '',
dictCode: '',
sortNumber: 100,
comments: ''
});
// 表单验证规则
const rules = reactive({
dictName: [
{
required: true,
message: '请输入字典名称',
type: 'string',
trigger: 'blur'
}
],
dictCode: [
{
required: true,
message: '请输入字典标识',
type: 'string',
trigger: 'blur'
}
],
sortNumber: [
{
required: true,
message: '请输入排序号',
type: 'number',
trigger: 'blur'
}
]
});
const { resetFields, validate, validateInfos } = useForm(form, rules);
/* 保存编辑 */
const save = () => {
validate()
.then(() => {
loading.value = true;
const saveOrUpdate = isUpdate.value ? updateDictionary : addDictionary;
saveOrUpdate(form)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
assignObject(form, props.data);
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
}
);
</script>

View File

@@ -0,0 +1,195 @@
<template>
<div class="ele-body">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-split-layout
width="266px"
allow-collapse
:right-style="{ overflow: 'hidden' }"
:style="{ minHeight: 'calc(100vh - 152px)' }"
>
<!-- 表格 -->
<ele-pro-table
ref="tableRef"
row-key="dictId"
:columns="columns"
:datasource="datasource"
v-model:current="current"
selection-type="radio"
:row-selection="{ columnWidth: 32 }"
:need-page="false"
:toolkit="[]"
height="calc(100vh - 290px)"
tools-theme="default"
bordered
class="sys-dict-table"
@done="done"
>
<template #toolbar>
<a-space :size="10">
<a-button type="primary" class="ele-btn-icon" @click="openEdit()">
<template #icon>
<plus-outlined />
</template>
<span>新建</span>
</a-button>
<a-button
type="primary"
:disabled="!current"
class="ele-btn-icon"
@click="openEdit(current)"
>
<template #icon>
<edit-outlined />
</template>
<span>修改</span>
</a-button>
<a-button
danger
type="primary"
:disabled="!current"
class="ele-btn-icon"
@click="remove"
>
<template #icon>
<delete-outlined />
</template>
<span>删除</span>
</a-button>
</a-space>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'dictName'">
<a-space :size="size">
<a-tooltip :title="`${record.dictCode}`">
{{ record.dictName }}
<!-- <span class="dict-code">{{ record.dictCode }}</span>-->
</a-tooltip>
</a-space>
</template>
</template>
</ele-pro-table>
<template #content>
<dict-data
v-if="current && current.dictId"
:dict-id="current.dictId"
/>
</template>
</ele-split-layout>
</a-card>
<!-- 编辑弹窗 -->
<dict-edit v-model:visible="showEdit" :data="editData" @done="reload" />
</div>
</template>
<script lang="ts" setup>
import { createVNode, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
ExclamationCircleOutlined
} from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem,
EleProTableDone
} from 'ele-admin-pro/es/ele-pro-table/types';
import DictData from './components/dict-data.vue';
import DictEdit from './components/dict-edit.vue';
import { listDictionaries, removeDictionary } from '@/api/system/dictionary';
import type { Dictionary } from '@/api/system/dictionary/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格列配置
const columns = ref<ColumnItem[]>([
{
key: 'index',
width: 45,
ellipsis: true,
align: 'center',
fixed: 'left',
hideInSetting: true,
customRender: ({ index }) => index + (tableRef.value?.tableIndex ?? 0)
},
{
title: '字典名称',
dataIndex: 'dictName',
key: 'dictName'
}
]);
// 表格选中数据
const current = ref<Dictionary | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 编辑回显数据
const editData = ref<Dictionary | null>(null);
// 表格数据源
const datasource: DatasourceFunction = () => {
return listDictionaries();
};
/* 表格渲染完成回调 */
const done: EleProTableDone<Dictionary> = (res) => {
if (res.data?.length) {
current.value = res.data[0];
}
};
/* 刷新表格 */
const reload = () => {
tableRef?.value?.reload();
};
/* 打开编辑弹窗 */
const openEdit = (row?: Dictionary | null) => {
editData.value = row ?? null;
showEdit.value = true;
};
/* 删除 */
const remove = () => {
Modal.confirm({
title: '提示',
content: '确定要删除选中的字典吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeDictionary(current.value?.dictId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
</script>
<script lang="ts">
export default {
name: 'SystemDictionary'
};
</script>
<style lang="less" scoped>
.sys-dict-table :deep(.ant-table-body) {
overflow: auto !important;
overflow: overlay !important;
}
.dict-code {
color: #cccccc;
}
</style>

View File

@@ -0,0 +1,195 @@
<template>
<div class="ele-body">
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<ele-split-layout
width="266px"
allow-collapse
:right-style="{ overflow: 'hidden' }"
:style="{ minHeight: 'calc(100vh - 152px)' }"
>
<!-- 表格 -->
<ele-pro-table
ref="tableRef"
row-key="dictId"
:columns="columns"
:datasource="datasource"
v-model:current="current"
selection-type="radio"
:row-selection="{ columnWidth: 32 }"
:need-page="false"
:toolkit="[]"
height="calc(100vh - 290px)"
tools-theme="default"
bordered
class="sys-dict-table"
@done="done"
>
<template #toolbar>
<a-space :size="10">
<a-button type="primary" class="ele-btn-icon" @click="openEdit()">
<template #icon>
<plus-outlined />
</template>
<span>新建</span>
</a-button>
<a-button
type="primary"
:disabled="!current"
class="ele-btn-icon"
@click="openEdit(current)"
>
<template #icon>
<edit-outlined />
</template>
<span>修改</span>
</a-button>
<a-button
danger
type="primary"
:disabled="!current"
class="ele-btn-icon"
@click="remove"
>
<template #icon>
<delete-outlined />
</template>
<span>删除</span>
</a-button>
</a-space>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'dictName'">
<a-space :size="size">
<a-tooltip :title="`${record.dictCode}`">
{{ record.dictName }}
<!-- <span class="dict-code">{{ record.dictCode }}</span>-->
</a-tooltip>
</a-space>
</template>
</template>
</ele-pro-table>
<template #content>
<dict-data
v-if="current && current.dictId"
:dict-id="current.dictId"
/>
</template>
</ele-split-layout>
</a-card>
<!-- 编辑弹窗 -->
<dict-edit v-model:visible="showEdit" :data="editData" @done="reload" />
</div>
</template>
<script lang="ts" setup>
import { createVNode, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
ExclamationCircleOutlined
} from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem,
EleProTableDone
} from 'ele-admin-pro/es/ele-pro-table/types';
import DictData from './components/dict-data.vue';
import DictEdit from './components/dict-edit.vue';
import { listDictionaries, removeDictionary } from '@/api/system/dictionary';
import type { Dictionary } from '@/api/system/dictionary/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格列配置
const columns = ref<ColumnItem[]>([
{
key: 'index',
width: 45,
ellipsis: true,
align: 'center',
fixed: 'left',
hideInSetting: true,
customRender: ({ index }) => index + (tableRef.value?.tableIndex ?? 0)
},
{
title: '字典名称',
dataIndex: 'dictName',
key: 'dictName'
}
]);
// 表格选中数据
const current = ref<Dictionary | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 编辑回显数据
const editData = ref<Dictionary | null>(null);
// 表格数据源
const datasource: DatasourceFunction = () => {
return listDictionaries();
};
/* 表格渲染完成回调 */
const done: EleProTableDone<Dictionary> = (res) => {
if (res.data?.length) {
current.value = res.data[0];
}
};
/* 刷新表格 */
const reload = () => {
tableRef?.value?.reload();
};
/* 打开编辑弹窗 */
const openEdit = (row?: Dictionary | null) => {
editData.value = row ?? null;
showEdit.value = true;
};
/* 删除 */
const remove = () => {
Modal.confirm({
title: '提示',
content: '确定要删除选中的字典吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeDictionary(current.value?.dictId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
</script>
<script lang="ts">
export default {
name: 'SystemDictionary'
};
</script>
<style lang="less" scoped>
.sys-dict-table :deep(.ant-table-body) {
overflow: auto !important;
overflow: overlay !important;
}
.dict-code {
color: #cccccc;
}
</style>

View File

@@ -0,0 +1,17 @@
<template>
<div style="padding-top: 80px">
<a-result status="403" title="403" sub-title="抱歉, 你无权访问该页面.">
<template #extra>
<router-link to="/">
<a-button type="primary">返回首页</a-button>
</router-link>
</template>
</a-result>
</div>
</template>
<script lang="ts">
export default {
name: 'Exception403'
};
</script>

View File

@@ -0,0 +1,70 @@
<template>
<div class="ele-body">
<a-card :bordered="false">
<div style="max-width: 960px; margin: 0 auto">
<a-result
status="error"
title="404"
sub-title="您访问的页面走丢了"
>
<!-- <div>无访问原因如下:</div>-->
<!-- <div class="error-tips-item">-->
<!-- <close-circle-outlined class="ele-text-danger" />-->
<!-- <div>您的账户已被冻结</div>-->
<!-- <a>立即解冻&gt;</a>-->
<!-- </div>-->
<!-- <div class="error-tips-item">-->
<!-- <close-circle-outlined class="ele-text-danger" />-->
<!-- <div>您的账户还不具备申请资格</div>-->
<!-- <a>立即咨询&gt;</a>-->
<!-- </div>-->
<!-- <div class="error-tips-item">-->
<!-- <close-circle-outlined class="ele-text-danger" />-->
<!-- <div>您的站点已过期</div>-->
<!-- <a>立即续费&gt;</a>-->
<!-- </div>-->
<template #extra>
<a-space size="middle">
<a-button type="primary" @click="openUrl('/')">返回首页</a-button>
<!-- <a-button @click="openNew('https://www.gxwebsoft.com')">技术支持</a-button>-->
</a-space>
</template>
</a-result>
</div>
</a-card>
</div>
</template>
<script lang="ts" setup>
import { CloseCircleOutlined } from '@ant-design/icons-vue';
import { openNew, openUrl } from '@/utils/common';
console.log('>>>>>');
const reload = () => {
return false;
};
reload();
</script>
<script lang="ts">
export default {
name: 'ResultFail'
};
</script>
<style lang="less" scoped>
.error-tips-item {
display: flex;
align-items: center;
margin-top: 16px;
& > div {
margin: 0 10px;
}
a {
white-space: nowrap;
}
}
</style>

View File

@@ -0,0 +1,17 @@
<template>
<div style="padding-top: 80px">
<a-result status="500" title="500" sub-title="抱歉, 服务器出错了.">
<template #extra>
<router-link to="/">
<a-button type="primary">返回首页</a-button>
</router-link>
</template>
</a-result>
</div>
</template>
<script lang="ts">
export default {
name: 'Exception500'
};
</script>

View File

@@ -0,0 +1,106 @@
<!-- 搜索表单 -->
<template>
<a-form
:label-col="
styleResponsive ? { xl: 7, lg: 5, md: 7, sm: 4 } : { flex: '90px' }
"
:wrapper-col="
styleResponsive ? { xl: 17, lg: 19, md: 17, sm: 20 } : { flex: '1' }
"
>
<a-row :gutter="8">
<a-col
v-bind="
styleResponsive
? { xl: 6, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 6 }
"
>
<a-form-item label="文件名称">
<a-input
v-model:value.trim="form.name"
placeholder="请输入"
allow-clear
/>
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive
? { xl: 6, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 6 }
"
>
<a-form-item label="文件路径">
<a-input
v-model:value.trim="form.path"
placeholder="请输入"
allow-clear
/>
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive
? { xl: 6, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 6 }
"
>
<a-form-item label="上传人">
<a-input
v-model:value.trim="form.createNickname"
placeholder="请输入"
allow-clear
/>
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive
? { xl: 6, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 6 }
"
>
<a-form-item class="ele-text-right" :wrapper-col="{ span: 24 }">
<a-space>
<a-button type="primary" @click="search">查询</a-button>
<a-button @click="reset">重置</a-button>
</a-space>
</a-form-item>
</a-col>
</a-row>
</a-form>
</template>
<script lang="ts" setup>
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import useFormData from '@/utils/use-form-data';
import type { FileRecordParam } from '@/api/system/file/model';
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const emit = defineEmits<{
(e: 'search', where?: FileRecordParam): void;
}>();
// 表单数据
const { form, resetFields } = useFormData<FileRecordParam>({
name: '',
path: '',
createNickname: ''
});
/* 搜索 */
const search = () => {
emit('search', form);
};
/* 重置 */
const reset = () => {
resetFields();
search();
};
</script>

View File

@@ -0,0 +1,244 @@
<template>
<div class="ele-body">
<a-card :bordered="false">
<!-- 搜索表单 -->
<file-search @search="reload" />
<!-- 表格 -->
<ele-pro-table
ref="tableRef"
row-key="id"
:columns="columns"
:datasource="datasource"
v-model:selection="selection"
:scroll="{ x: 800 }"
cache-key="proSystemFileTable"
>
<template #toolbar>
<a-space>
<a-upload :show-upload-list="false" :customRequest="onUpload">
<a-button type="primary" class="ele-btn-icon">
<template #icon>
<upload-outlined />
</template>
<span>上传</span>
</a-button>
</a-upload>
<a-button
danger
type="primary"
class="ele-btn-icon"
@click="removeBatch"
>
<template #icon>
<delete-outlined />
</template>
<span>删除</span>
</a-button>
</a-space>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'path'">
<a @click="openNew(getUrl(record.path))" target="_blank">
{{ record.path }}
</a>
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<a :href="record.url" target="_blank">下载</a>
<a-divider type="vertical" />
<a-popconfirm
placement="topRight"
title="确定要删除此文件吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
</div>
</template>
<script lang="ts" setup>
import { createVNode, ref } from 'vue';
import { message, Modal } from 'ant-design-vue/es';
import {
UploadOutlined,
DeleteOutlined,
ExclamationCircleOutlined
} from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro/es';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import { messageLoading, toDateString } from 'ele-admin-pro/es';
import FileSearch from './components/file-search.vue';
import {
pageFiles,
removeFile,
removeFiles,
uploadFile
} from '@/api/system/file';
import type { FileRecord, FileRecordParam } from '@/api/system/file/model';
import { getUrl, openNew } from '@/utils/common';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: 'ID',
dataIndex: 'id',
width: 80,
hideInTable: true
},
{
title: '文件名称',
dataIndex: 'name',
key: 'name',
sorter: true,
showSorterTooltip: false,
ellipsis: true
},
{
title: '文件路径',
key: 'path',
dataIndex: 'path',
sorter: true,
showSorterTooltip: false,
ellipsis: true
},
{
title: '文件大小',
dataIndex: 'length',
sorter: true,
showSorterTooltip: false,
ellipsis: true,
customRender: ({ text }) => {
if (text < 1024) {
return text + 'B';
} else if (text < 1024 * 1024) {
return (text / 1024).toFixed(1) + 'KB';
} else if (text < 1024 * 1024 * 1024) {
return (text / 1024 / 1024).toFixed(1) + 'M';
} else {
return (text / 1024 / 1024 / 1024).toFixed(1) + 'G';
}
},
width: 120
},
{
title: '上传人',
dataIndex: 'createNickname',
sorter: true,
showSorterTooltip: false,
ellipsis: true,
width: 120
},
{
title: '上传时间',
dataIndex: 'createTime',
sorter: true,
showSorterTooltip: false,
ellipsis: true,
customRender: ({ text }) => toDateString(text),
width: 160
},
{
title: '操作',
key: 'action',
width: 120,
align: 'center'
}
]);
// 表格选中数据
const selection = ref<FileRecord[]>([]);
// 表格数据源
const datasource: DatasourceFunction = ({ page, limit, where, orders }) => {
return pageFiles({ ...where, ...orders, page, limit });
};
/* 搜索 */
const reload = (where?: FileRecordParam) => {
selection.value = [];
tableRef?.value?.reload({ page: 1, where });
};
/* 删除单个 */
const remove = (row: FileRecord) => {
const hide = messageLoading('请求中..', 0);
removeFile(row.id)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的文件吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = messageLoading('请求中..', 0);
removeFiles(selection.value.map((d) => d.id))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 上传 */
const onUpload = ({ file }) => {
if (file.size / 1024 / 1024 > 100) {
message.error('大小不能超过 100MB');
return false;
}
const hide = messageLoading({
content: '上传中..',
duration: 0,
mask: true
});
uploadFile(file)
.then(() => {
hide();
message.success('上传成功');
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
return false;
};
</script>
<script lang="ts">
export default {
name: 'SystemFile'
};
</script>

View File

@@ -0,0 +1,202 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="600"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑等级' : '添加等级'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
>
<a-form-item label="等级名称" name="name">
<a-input
allow-clear
placeholder="请输入等级名称"
v-model:value="form.name"
/>
</a-form-item>
<a-form-item label="等级权重" name="weight">
<a-input
allow-clear
placeholder="请输入等级权重"
v-model:value="form.weight"
/>
</a-form-item>
<a-form-item label="升级条件" name="upgrade">
<a-input
allow-clear
placeholder="请输入升级条件"
v-model:value="form.upgrade"
/>
</a-form-item>
<a-form-item label="会员权益" name="equity">
<a-input
allow-clear
placeholder="请输入会员权益"
v-model:value="form.equity"
/>
</a-form-item>
<a-form-item label="备注" name="comments">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入备注"
v-model:value="form.comments"
/>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject } from 'ele-admin-pro';
import { addGrade, updateGrade } from '@/api/user/grade';
import { Grade } from '@/api/user/grade/model';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
import { FormInstance } from 'ant-design-vue/es/form';
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Grade | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 表格选中数据
const formRef = ref<FormInstance | null>(null);
// 用户信息
const form = reactive<Grade>({
gradeId: undefined,
name: '',
weight: undefined,
upgrade: undefined,
equity: '',
status: undefined,
comments: undefined,
sortNumber: undefined,
userId: undefined,
createTime: '',
updateTime: ''
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
name: [
{
required: true,
type: 'string',
message: '请输入等级名称',
trigger: 'blur'
}
],
weight: [
{
required: true,
type: 'string',
message: '请输入等级权重',
trigger: 'blur'
}
],
upgrade: [
{
required: true,
type: 'string',
message: '请输入升级条件',
trigger: 'blur'
}
]
});
const { resetFields } = useForm(form, rules);
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form
};
const saveOrUpdate = isUpdate.value ? updateGrade : addGrade;
saveOrUpdate(formData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
assignObject(form, props.data);
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
}
);
</script>
<style lang="less">
.tab-pane {
min-height: 300px;
}
.ml-10 {
margin-left: 5px;
}
.upload-text {
margin-right: 70px;
}
</style>

View File

@@ -0,0 +1,42 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-button type="primary" class="ele-btn-icon" @click="add">
<template #icon>
<PlusOutlined />
</template>
<span>添加</span>
</a-button>
</a-space>
</template>
<script lang="ts" setup>
import { PlusOutlined } from '@ant-design/icons-vue';
import type { GradeParam } from '@/api/user/grade/model';
import { watch } from 'vue';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: GradeParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
// 新增
const add = () => {
emit('add');
};
watch(
() => props.selection,
() => {}
);
</script>

View File

@@ -0,0 +1,292 @@
<template>
<div class="page">
<div class="ele-body">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="gradeId"
:columns="columns"
:datasource="datasource"
v-model:selection="selection"
:customRow="customRow"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'gradeName'">
<a-avatar
:size="30"
:src="`${record.gradeAvatar}`"
style="margin-right: 4px"
:srcset="`https://file.wsdns.cn/${record.gradeAvatar}`"
>
<template #icon>
<UserOutlined />
</template>
</a-avatar>
<a-tooltip title="查看详情">
<a href="#" @click="openInfo(record)">{{ record.gradeName }}</a>
</a-tooltip>
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">启用</a-tag>
<a-tag v-if="record.status === 1" color="red">禁用</a-tag>
</template>
<template v-if="column.key === 'createTime'">
<a-tooltip :title="`${toDateString(record.createTime)}`">
{{ timeAgo(record.createTime) }}
</a-tooltip>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical" />
<a-popconfirm
title="确定要删除此记录吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<GradeEdit v-model:visible="showEdit" :data="current" @done="reload" />
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, createVNode, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import {
ExclamationCircleOutlined,
UserOutlined
} from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import { toDateString } from 'ele-admin-pro';
import Search from './components/search.vue';
import GradeEdit from './components/grade-edit.vue';
import { pageGrade, removeGrade, removeBatchGrade } from '@/api/user/grade';
import { timeAgo } from 'ele-admin-pro';
import type { Grade, GradeParam } from '@/api/user/grade/model';
import { useUserStore } from '@/store/modules/user';
const userStore = useUserStore();
// 当前用户信息
const loginUser = computed(() => userStore.info ?? {});
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<Grade[]>([]);
// 当前编辑数据
const current = ref<Grade | null>(null);
// 是否显示资产详情
const showInfo = ref(false);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
const loading = ref(true);
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.progress = filters.progress;
where.gradeSource = filters.gradeSource;
where.gradeType = filters.gradeType;
where.status = filters.status;
}
return pageGrade({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: 'ID',
dataIndex: 'gradeId',
width: 80,
hideInTable: true
},
{
title: '等级名称',
dataIndex: 'name'
},
{
title: '等级权重',
dataIndex: 'weight'
},
{
title: '升级条件',
dataIndex: 'upgrade'
},
{
title: '等级权益',
dataIndex: 'equity'
},
{
title: '状态',
dataIndex: 'status',
key: 'status'
},
{
title: '创建时间',
dataIndex: 'createTime',
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd')
},
{
title: '操作',
key: 'action',
width: 200,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: GradeParam) => {
console.log(where);
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: Grade) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 打开用户详情弹窗 */
const openInfo = (row?: Grade) => {
current.value = row ?? null;
showInfo.value = true;
};
/* 删除单个 */
const remove = (row: Grade) => {
const hide = message.loading('请求中..', 0);
removeGrade(row.gradeId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量转移 */
const batchMove = (userId) => {
console.log(userId, '批量转移0000');
console.log(selection.value);
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchGrade(
selection.value.map((d) => {
if (loginUser.value.userId === d.userId) {
return d.gradeId;
}
})
)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: Grade) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'Grade'
};
</script>
<style lang="less" scoped>
.sys-org-table :deep(.ant-table-body) {
overflow: auto !important;
overflow: overlay !important;
}
.sys-org-table :deep(.ant-table-pagination.ant-pagination) {
padding: 0 4px;
margin-bottom: 0;
}
</style>

View File

@@ -0,0 +1,115 @@
<!-- 搜索表单 -->
<template>
<a-form
:label-col="
styleResponsive ? { xl: 7, lg: 5, md: 7, sm: 4 } : { flex: '90px' }
"
:wrapper-col="
styleResponsive ? { xl: 17, lg: 19, md: 17, sm: 20 } : { flex: '1' }
"
>
<a-row :gutter="8">
<a-col
v-bind="
styleResponsive
? { xl: 6, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 6 }
"
>
<a-form-item label="用户账号">
<a-input
v-model:value.trim="form.username"
placeholder="请输入"
allow-clear
/>
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive
? { xl: 6, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 6 }
"
>
<a-form-item label="昵称">
<a-input
v-model:value.trim="form.nickname"
placeholder="请输入"
allow-clear
/>
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive
? { xl: 6, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 6 }
"
>
<a-form-item label="登录时间">
<a-range-picker
v-model:value="dateRange"
value-format="YYYY-MM-DD"
class="ele-fluid"
/>
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive
? { xl: 6, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 6 }
"
>
<a-form-item class="ele-text-right" :wrapper-col="{ span: 24 }">
<a-space>
<a-button type="primary" @click="search">查询</a-button>
<a-button @click="reset">重置</a-button>
</a-space>
</a-form-item>
</a-col>
</a-row>
</a-form>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import useFormData from '@/utils/use-form-data';
import type { LoginRecordParam } from '@/api/system/login-record/model';
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const emit = defineEmits<{
(e: 'search', where?: LoginRecordParam): void;
}>();
// 表单数据
const { form, resetFields } = useFormData<LoginRecordParam>({
username: '',
nickname: ''
});
// 日期范围选择
const dateRange = ref<[string, string]>(['', '']);
/* 搜索 */
const search = () => {
const [d1, d2] = dateRange.value ?? [];
emit('search', {
...form,
createTimeStart: d1 ? d1 + ' 00:00:00' : '',
createTimeEnd: d2 ? d2 + ' 23:59:59' : ''
});
};
/* 重置 */
const reset = () => {
resetFields();
dateRange.value = ['', ''];
search();
};
</script>

View File

@@ -0,0 +1,235 @@
<template>
<div class="ele-body">
<a-card :bordered="false">
<!-- 搜索表单 -->
<login-record-search @search="reload" />
<!-- 表格 -->
<ele-pro-table
ref="tableRef"
row-key="id"
:columns="columns"
:datasource="datasource"
:scroll="{ x: 900 }"
cache-key="proSystemLoginRecordTable"
>
<template #toolbar>
<a-space>
<a-button type="primary" class="ele-btn-icon" @click="exportData">
<template #icon>
<download-outlined />
</template>
<span>导出</span>
</a-button>
</a-space>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'loginType'">
<a-tag v-if="record.loginType === 0" color="green">登录成功</a-tag>
<a-tag v-else-if="record.loginType === 1" color="red">
登录失败
</a-tag>
<a-tag v-else-if="record.loginType === 2">退出登录</a-tag>
<a-tag v-else-if="record.loginType === 3" color="orange">
刷新TOKEN
</a-tag>
</template>
</template>
</ele-pro-table>
</a-card>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { message } from 'ant-design-vue/es';
import { utils, writeFile } from 'xlsx';
import { DownloadOutlined } from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro/es';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import { messageLoading, toDateString } from 'ele-admin-pro/es';
import LoginRecordSearch from './components/login-record-search.vue';
import {
pageLoginRecords,
listLoginRecords
} from '@/api/system/login-record';
import type { LoginRecordParam } from '@/api/system/login-record/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格列配置
const columns = ref<ColumnItem[]>([
{
key: 'index',
width: 80,
align: 'center',
fixed: 'left',
hideInSetting: true,
customRender: ({ index }) => index + (tableRef.value?.tableIndex ?? 0)
},
{
title: '账号',
dataIndex: 'username',
sorter: true,
showSorterTooltip: false
},
{
title: '昵称',
dataIndex: 'nickname',
sorter: true,
showSorterTooltip: false
},
{
title: 'IP地址',
dataIndex: 'ip',
sorter: true,
showSorterTooltip: false,
ellipsis: true
},
{
title: '设备型号',
dataIndex: 'device',
sorter: true,
showSorterTooltip: false,
ellipsis: true
},
{
title: '操作系统',
dataIndex: 'os',
sorter: true,
showSorterTooltip: false,
ellipsis: true
},
{
title: '浏览器',
dataIndex: 'browser',
sorter: true,
showSorterTooltip: false,
ellipsis: true
},
{
title: '操作类型',
key: 'loginType',
dataIndex: 'loginType',
sorter: true,
showSorterTooltip: false,
width: 120,
filters: [
{
text: '登录成功',
value: 0
},
{
text: '登录失败',
value: 1
},
{
text: '退出登录',
value: 2
},
{
text: '刷新TOKEN',
value: 3
}
],
filterMultiple: false
},
{
title: '备注',
dataIndex: 'comments',
sorter: true,
showSorterTooltip: false,
ellipsis: true
},
{
title: '登录时间',
dataIndex: 'createTime',
sorter: true,
showSorterTooltip: false,
ellipsis: true,
customRender: ({ text }) => toDateString(text)
}
]);
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
return pageLoginRecords({
...where,
...orders,
...filters,
page,
limit
});
};
/* 刷新表格 */
const reload = (where?: LoginRecordParam) => {
tableRef?.value?.reload({ page: 1, where });
};
/* 导出数据 */
const exportData = () => {
const array = [
[
'账号',
'昵称',
'IP地址',
'设备型号',
'操作系统',
'浏览器',
'操作类型',
'备注',
'登录时间'
]
];
// 请求查询全部接口
const hide = messageLoading('请求中..', 0);
tableRef.value?.doRequest(({ where, orders, filters }) => {
listLoginRecords({ ...where, ...orders, ...filters })
.then((data) => {
hide();
data.forEach((d) => {
array.push([
d.username,
d.nickname,
d.ip,
d.device,
d.os,
d.browser,
['登录成功', '登录失败', '退出登录', '刷新TOKEN'][d.loginType],
d.comments,
toDateString(d.createTime)
]);
});
writeFile(
{
SheetNames: ['Sheet1'],
Sheets: {
Sheet1: utils.aoa_to_sheet(array)
}
},
'登录日志.xlsx'
);
})
.catch((e) => {
hide();
message.error(e.message);
});
});
};
</script>
<script lang="ts">
export default {
name: 'SystemLoginRecord'
};
</script>

View File

@@ -0,0 +1,192 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="540"
:visible="visible"
:confirm-loading="loading"
:title="`菜单克隆`"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 5, sm: 4, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 18, sm: 20, xs: 24 } : { flex: '1' }
"
>
<a-form-item label="选择模板" name="tenantName">
<SelectTenant v-model:value="tenantName" @done="onPromoter" />
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { message } from 'ant-design-vue/es';
import type { FormInstance, Rule } from 'ant-design-vue/es/form';
import { isExternalLink } from 'ele-admin-pro/es';
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import useFormData from '@/utils/use-form-data';
import { clone } from '@/api/system/menu';
import type { Menu } from '@/api/system/menu/model';
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Menu | null;
// 上级菜单id
parentId?: number;
// 全部菜单数据
menuList: Menu[];
}>();
//
const formRef = ref<FormInstance | null>(null);
// 是否是修改
const isUpdate = ref(false);
// 提交状态
const loading = ref(false);
const tenantName = ref('请选择租户');
// 表单数据
const { form, resetFields, assignFields } = useFormData<Menu>({
title: '',
icon: '',
path: '',
component: '',
tenantId: undefined,
tenantName: ''
});
// 表单验证规则
const rules = reactive<Record<string, Rule[]>>({
tenantName: [
{
required: true,
message: '请选择要克隆的模版',
type: 'string',
trigger: 'blur'
}
]
});
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const menuForm = {
...form
};
clone(menuForm)
.then((msg) => {
loading.value = false;
updateVisible(false);
emit('done');
message.success(msg);
window.open('/', '_self');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
/* 判断是否是目录 */
const isDirectory = (d: Menu) => {
return !!d.children?.length && !d.component;
};
const onPromoter = (e) => {
tenantName.value = e.tenantName;
form.tenantId = e.tenantId;
form.tenantName = e.tenantName;
};
// 查询租户列表
// const tenantList = ref<Tenant[]>([]);
// const reload = (tenantName?: any) => {
// listTenant({ tenantName }).then((result) => {
// tenantList.value = result;
// });
// };
//
// reload();
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
const isExternal = isExternalLink(props.data.path);
const isInner = isExternalLink(props.data.component);
// menuType 对应的值与后端不一致在前端处理
const menuType =
props.data.menuType === 1 ? 2 : isDirectory(props.data) ? 0 : 1;
assignFields({
...props.data,
menuType,
openType: isExternal ? 2 : isInner ? 1 : 0,
parentId:
props.data.parentId === 0 ? undefined : props.data.parentId
});
isUpdate.value = true;
} else {
form.parentId = props.parentId;
isUpdate.value = false;
}
} else {
resetFields();
formRef.value?.clearValidate();
}
}
);
</script>
<script lang="ts">
import * as icons from '@/layout/menu-icons';
export default {
components: icons,
data() {
return {
iconData: [
{
title: '已引入的图标',
icons: Object.keys(icons)
}
]
};
}
};
</script>

View File

@@ -0,0 +1,469 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="740"
:visible="visible"
:confirm-loading="loading"
:title="isUpdate ? '修改菜单' : '新建菜单'"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 6, sm: 4, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 18, sm: 20, xs: 24 } : { flex: '1' }
"
>
<a-row :gutter="16">
<a-col
v-bind="styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 12 }"
>
<a-form-item label="上级菜单" name="parentId">
<a-tree-select
allow-clear
show-search
tree-node-filter-prop="title"
:tree-data="menuList"
placeholder="请选择上级菜单"
:value="form.parentId || undefined"
:dropdown-style="{ maxHeight: '360px', overflow: 'auto' }"
@update:value="(value?: number) => (form.parentId = value)"
/>
</a-form-item>
<a-form-item label="菜单名称" name="title">
<a-input
allow-clear
placeholder="请输入菜单名称"
v-model:value="form.title"
@pressEnter="save"
/>
</a-form-item>
</a-col>
<a-col
v-bind="styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 12 }"
>
<a-form-item label="菜单类型" name="menuType">
<a-radio-group
v-model:value="form.menuType"
@change="onMenuTypeChange"
>
<a-radio :value="0">目录</a-radio>
<a-radio :value="1">菜单</a-radio>
<a-radio :value="2">按钮</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="打开方式">
<a-radio-group
v-model:value="form.openType"
:disabled="form.menuType === 0 || form.menuType === 2"
@change="onOpenTypeChange"
>
<a-radio :value="0">组件</a-radio>
<a-radio :value="1">内链</a-radio>
<a-radio :value="2">外链</a-radio>
</a-radio-group>
</a-form-item>
</a-col>
</a-row>
<div style="margin-bottom: 22px">
<a-divider />
</div>
<a-row :gutter="16">
<a-col
v-bind="styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 12 }"
>
<a-form-item label="菜单图标" name="icon">
<ele-icon-picker
:data="iconData"
:allow-search="false"
v-model:value="form.icon"
placeholder="请选择菜单图标"
:disabled="form.menuType === 2"
>
<template #icon="{ icon }">
<component :is="icon" />
</template>
</ele-icon-picker>
</a-form-item>
<a-form-item name="path">
<template #label>
<a-tooltip
v-if="form.openType === 2"
title="需要以`http://`、`https://`、`//`开头"
>
<question-circle-outlined
style="vertical-align: -2px; margin-right: 4px"
/>
</a-tooltip>
<span>{{ form.openType === 2 ? '外链地址' : '路由地址' }}</span>
</template>
<a-input
allow-clear
v-model:value="form.path"
:disabled="form.menuType === 2"
:placeholder="
form.openType === 2 ? '请输入外链地址' : '请输入路由地址'
"
/>
</a-form-item>
<a-form-item name="component">
<template #label>
<a-tooltip
v-if="form.openType === 1"
title="需要以`http://`、`https://`、`//`开头"
>
<question-circle-outlined
style="vertical-align: -2px; margin-right: 4px"
/>
</a-tooltip>
<span>{{ form.openType === 1 ? '内链地址' : '组件路径' }}</span>
</template>
<a-input
allow-clear
v-model:value="form.component"
:disabled="
form.menuType === 0 ||
form.menuType === 2 ||
form.openType === 2
"
:placeholder="
form.openType === 1 ? '请输入内链地址' : '请输入组件路径'
"
/>
<!-- <div class="ele-text-placeholder" v-if="form.modulesUrl">-->
<!-- {{ `${form.modulesUrl}${form.component}?token=TOKEN` }}-->
<!-- </div>-->
</a-form-item>
<a-form-item
name="modulesUrl"
v-if="form.menuType == 1 && form.openType === 0"
>
<template #label>
<a-tooltip title="适用于分布式部署">
<question-circle-outlined
style="vertical-align: -2px; margin-right: 4px"
/>
</a-tooltip>
<span>节点域名</span>
</template>
<a-input
allow-clear
v-model:value="form.modulesUrl"
placeholder="请输入模块URL"
@change="onClose"
/>
</a-form-item>
</a-col>
<a-col
v-bind="styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 12 }"
>
<a-form-item label="权限标识" name="authority">
<a-input
allow-clear
placeholder="请输入权限标识"
v-model:value="form.authority"
:disabled="
form.menuType === 0 ||
(form.menuType === 1 && form.openType === 2)
"
@pressEnter="save"
/>
</a-form-item>
<a-form-item label="排序号" name="sortNumber">
<a-input-number
:min="0"
:max="99999"
class="ele-fluid"
placeholder="请输入排序号"
v-model:value="form.sortNumber"
@pressEnter="save"
/>
</a-form-item>
<a-form-item label="是否展示">
<a-switch
checked-children=""
un-checked-children=""
:checked="form.hide === 0"
:disabled="form.menuType === 2"
@update:checked="updateHideValue"
/>
<a-tooltip
title="选择不展示只注册路由不展示在侧边栏, 比如添加页面应该选择不展示"
>
<question-circle-outlined
style="vertical-align: -4px; margin-left: 16px"
/>
</a-tooltip>
</a-form-item>
<a-form-item
label="选择节点"
name="modulesUrl"
v-if="form.menuType == 1 && form.openType === 0"
>
<SelectModules
:placeholder="`请选择模块所在的服务器`"
v-model:value="form.modules"
:disabled="isUpdate"
@done="chooseModules"
/>
</a-form-item>
</a-col>
</a-row>
<a-form-item
label="路由元数据"
name="meta"
:label-col="
styleResponsive ? { md: 3, sm: 4, xs: 24 } : { flex: '90px' }
"
:wrapper-col="
styleResponsive ? { md: 21, sm: 20, xs: 24 } : { flex: '1' }
"
>
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入JSON格式的路由元数据"
v-model:value="form.meta"
/>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { message } from 'ant-design-vue/es';
import type { FormInstance, Rule } from 'ant-design-vue/es/form';
import { QuestionCircleOutlined } from '@ant-design/icons-vue';
import { isExternalLink } from 'ele-admin-pro/es';
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import useFormData from '@/utils/use-form-data';
import { addMenu, updateMenu } from '@/api/system/menu';
import type { Menu } from '@/api/system/menu/model';
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Menu | null;
// 上级菜单id
parentId?: number;
// 全部菜单数据
menuList: Menu[];
}>();
//
const formRef = ref<FormInstance | null>(null);
// 是否是修改
const isUpdate = ref(false);
// 提交状态
const loading = ref(false);
// 表单数据
const { form, resetFields, assignFields } = useFormData<Menu>({
menuId: undefined,
parentId: undefined,
modules: undefined,
modulesUrl: undefined,
title: '',
menuType: 0,
openType: 0,
icon: '',
path: '',
component: '',
authority: '',
plugUrl: '',
sortNumber: undefined,
hide: 0,
meta: ''
});
// 表单验证规则
const rules = reactive<Record<string, Rule[]>>({
title: [
{
required: true,
message: '请输入菜单名称',
type: 'string',
trigger: 'blur'
}
],
sortNumber: [
{
required: true,
message: '请输入排序号',
type: 'number',
trigger: 'blur'
}
],
meta: [
{
type: 'string',
validator: async (_rule: Rule, value: string) => {
if (value) {
const msg = '请输入正确的JSON格式';
try {
const obj = JSON.parse(value);
if (typeof obj !== 'object' || obj === null) {
return Promise.reject(msg);
}
} catch (_e) {
return Promise.reject(msg);
}
}
return Promise.resolve();
},
trigger: 'blur'
}
]
});
const chooseModules = (e) => {
form.modulesUrl = e.modulesUrl;
form.modules = e.modules;
};
const onClose = () => {
if (form.modulesUrl == '') {
form.modules = '';
}
};
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const menuForm = {
...form,
// menuType 对应的值与后端不一致在前端处理
menuType: form.menuType === 2 ? 1 : 0,
parentId: form.parentId || 0
};
const saveOrUpdate = isUpdate.value ? updateMenu : addMenu;
saveOrUpdate(menuForm)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
/* menuType选择改变 */
const onMenuTypeChange = () => {
form.sortNumber = undefined;
if (form.menuType === 0) {
form.authority = '';
form.openType = 0;
form.component = '';
} else if (form.menuType === 1) {
if (form.openType === 2) {
form.authority = '';
}
} else if (form.menuType === 2) {
form.sortNumber = 0;
} else {
form.openType = 0;
form.icon = '';
form.path = '';
form.component = '';
form.hide = 0;
}
};
/* openType选择改变 */
const onOpenTypeChange = () => {
if (form.openType === 2) {
form.component = '';
form.authority = '';
}
};
const updateHideValue = (value: boolean) => {
form.hide = value ? 0 : 1;
};
/* 判断是否是目录 */
const isDirectory = (d: Menu) => {
return !!d.children?.length && !d.component;
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
const isExternal = isExternalLink(props.data.path);
const isInner = isExternalLink(props.data.component);
// menuType 对应的值与后端不一致在前端处理
const menuType =
props.data.menuType === 1 ? 2 : isDirectory(props.data) ? 0 : 1;
assignFields({
...props.data,
menuType,
openType: isExternal ? 2 : isInner ? 1 : 0,
parentId:
props.data.parentId === 0 ? undefined : props.data.parentId
});
isUpdate.value = true;
} else {
form.parentId = props.parentId;
isUpdate.value = false;
}
} else {
resetFields();
formRef.value?.clearValidate();
}
}
);
</script>
<script lang="ts">
import * as icons from '@/layout/menu-icons';
export default {
components: icons,
data() {
return {
iconData: [
{
title: '已引入的图标',
icons: Object.keys(icons)
}
]
};
}
};
</script>

View File

@@ -0,0 +1,168 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="520"
:visible="visible"
:confirm-loading="loading"
title="发布插件"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
cancelText="取消"
okText="确定发布"
@ok="save"
>
<a-form ref="formRef" :model="form" :rules="rules">
<a-alert
:description="`审核通过后,该插件将展示在扩展插件模块,可供其他用户安装和使用后,获得销售分成。`"
type="success"
closable
show-icon
>
<template #icon><SmileOutlined /></template>
</a-alert>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { message } from 'ant-design-vue/es';
import type { FormInstance, Rule } from 'ant-design-vue/es/form';
import useFormData from '@/utils/use-form-data';
import { createPlug } from '@/api/system/plug';
import type { Plug } from '@/api/system/plug/model';
import { RuleObject } from 'ant-design-vue/es/form';
import { isExternalLink } from 'ele-admin-pro';
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Plug | null;
// 上级菜单id
parentId?: number;
// 全部菜单数据
menuList: Plug[];
}>();
//
const formRef = ref<FormInstance | null>(null);
// 是否是修改
const isUpdate = ref(false);
// 提交状态
const loading = ref(false);
const appid = ref(undefined);
// 表单数据
const { form, resetFields, assignFields } = useFormData<Plug>({
parentId: undefined
});
// 表单验证规则
const rules = reactive<Record<string, Rule[]>>({
appId: [
{
required: true,
message: '请选择要绑定的应用',
type: 'number',
trigger: 'blur',
validator: async (_rule: RuleObject, value: string) => {
if (appid.value == undefined) {
return Promise.reject('请选择要绑定的应用');
}
return Promise.resolve();
}
}
]
});
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
form.parentId = props.data?.menuId;
createPlug(form)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
/* 判断是否是目录 */
const isDirectory = (d: Plug) => {
return !!d.children?.length && !d.component;
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
const isExternal = isExternalLink(props.data.path);
const isInner = isExternalLink(props.data.component);
// menuType 对应的值与后端不一致在前端处理
const menuType =
props.data.menuType === 1 ? 2 : isDirectory(props.data) ? 0 : 1;
assignFields({
...props.data,
menuType,
openType: isExternal ? 2 : isInner ? 1 : 0,
parentId:
props.data.parentId === 0 ? undefined : props.data.parentId
});
isUpdate.value = true;
} else {
form.parentId = props.parentId;
isUpdate.value = false;
}
} else {
resetFields();
formRef.value?.clearValidate();
}
}
);
</script>
<script lang="ts">
import * as icons from '@/layout/menu-icons';
export default {
components: icons,
data() {
return {
iconData: [
{
title: '已引入的图标',
icons: Object.keys(icons)
}
]
};
}
};
</script>

View File

@@ -0,0 +1,106 @@
<!-- 搜索表单 -->
<template>
<a-form
:label-col="
styleResponsive ? { xl: 7, lg: 5, md: 7, sm: 4 } : { flex: '90px' }
"
:wrapper-col="
styleResponsive ? { xl: 17, lg: 19, md: 17, sm: 20 } : { flex: '1' }
"
>
<a-row :gutter="8">
<a-col
v-bind="
styleResponsive
? { xl: 6, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 6 }
"
>
<a-form-item label="菜单名称">
<a-input
v-model:value.trim="form.title"
placeholder="请输入"
allow-clear
/>
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive
? { xl: 6, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 6 }
"
>
<a-form-item label="菜单地址">
<a-input
v-model:value.trim="form.path"
placeholder="请输入"
allow-clear
/>
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive
? { xl: 6, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 6 }
"
>
<a-form-item label="权限标识">
<a-input
v-model:value.trim="form.authority"
placeholder="请输入"
allow-clear
/>
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive
? { xl: 6, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 6 }
"
>
<a-form-item class="ele-text-right" :wrapper-col="{ span: 24 }">
<a-space>
<a-button type="primary" @click="search">查询</a-button>
<a-button @click="reset">重置</a-button>
</a-space>
</a-form-item>
</a-col>
</a-row>
</a-form>
</template>
<script lang="ts" setup>
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import useFormData from '@/utils/use-form-data';
import type { MenuParam } from '@/api/system/menu/model';
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const emit = defineEmits<{
(e: 'search', where?: MenuParam): void;
}>();
// 表单数据
const { form, resetFields } = useFormData<MenuParam>({
title: '',
path: '',
authority: ''
});
/* 搜索 */
const search = () => {
emit('search', form);
};
/* 重置 */
const reset = () => {
resetFields();
search();
};
</script>

View File

@@ -0,0 +1,383 @@
<template>
<div class="ele-body">
<a-card :bordered="false">
<!-- 搜索表单 -->
<menu-search @search="reload" />
<!-- 表格 -->
<ele-pro-table
ref="tableRef"
row-key="menuId"
:columns="columns"
:datasource="datasource"
:parse-data="parseData"
:need-page="false"
:expand-icon-column-index="1"
:expanded-row-keys="expandedRowKeys"
:scroll="{ x: 1200 }"
cache-key="proSystemMenuTable"
:customRow="customRow"
@done="onDone"
@expand="onExpand"
>
<template #toolbar>
<a-space>
<a-button type="primary" class="ele-btn-icon" @click="openEdit()">
<template #icon>
<plus-outlined />
</template>
<span>新建</span>
</a-button>
<a-button type="dashed" class="ele-btn-icon" @click="expandAll">
展开全部
</a-button>
<a-button type="dashed" class="ele-btn-icon" @click="foldAll">
折叠全部
</a-button>
<!-- <a-button type="dashed" class="ele-btn-icon" @click="cloneMenu">-->
<!-- 一键克隆-->
<!-- </a-button>-->
</a-space>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'menuType'">
<a-tag v-if="isExternalLink(record.path)" color="red">外链</a-tag>
<a-tag v-else-if="isExternalLink(record.component)" color="orange">
内链
</a-tag>
<a-tag v-else-if="isDirectory(record)" color="blue">目录</a-tag>
<a-tag v-else-if="record.menuType === 0" color="green">菜单</a-tag>
<a-tag v-else-if="record.menuType === 1">按钮</a-tag>
</template>
<template v-else-if="column.key === 'title'">
<component v-if="record.icon" :is="record.icon" />
<a-tooltip title="点击复制">
<span style="padding-left: 8px" @click="copyText(record.title)">
{{ record.title }}
</span>
</a-tooltip>
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<a @click="openEdit(null, record.menuId)">添加</a>
<a-divider type="vertical" />
<a @click="openEdit(record)">修改</a>
<!-- <a-divider type="vertical" />-->
<!-- <a @click="addPlug(record)">插件</a>-->
<a-divider type="vertical" />
<a-popconfirm
placement="topRight"
title="确定要删除此菜单吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
<template v-if="column.key === 'path'">
<div v-if="record.modulesUrl">
<a-tooltip
:title="`${
record.modulesUrl
? record.modulesUrl + record.path + '?token=' + token
: ''
}`"
>
<a
class="ele-text-primary"
@click="
openUrl(`${record.modulesUrl}${record.path}?token=${token}`)
"
>{{ record.path }}</a
>
</a-tooltip>
</div>
<span v-else>{{ record.path }}</span>
</template>
<template v-if="column.key === 'component'">
<div v-if="record.modulesUrl">
<a-tooltip
:title="`${
record.modulesUrl ? record.modulesUrl + record.path : ''
}`"
>
<span class="ele-text-primary">{{ record.component }}</span>
</a-tooltip>
</div>
<div v-else>{{ record.component }}</div>
</template>
<template v-if="column.key === 'authority'">
{{ record.authority }}
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<menu-edit
v-model:visible="showEdit"
:data="current"
:parent-id="parentId"
:menu-list="menuData"
@done="reload"
/>
<!-- 制作插件 -->
<menu-plug
v-model:visible="showPlug"
:data="current"
:parent-id="parentId"
:menu-list="menuData"
@done="reload"
/>
<clone v-model:visible="showClone" @done="reload" />
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { message } from 'ant-design-vue/es';
import { PlusOutlined } from '@ant-design/icons-vue';
import type {
DatasourceFunction,
ColumnItem,
EleProTableDone
} from 'ele-admin-pro/es/ele-pro-table/types';
import MenuSearch from './components/menu-search.vue';
import {
messageLoading,
toDateString,
isExternalLink,
toTreeData,
eachTreeData
} from 'ele-admin-pro/es';
import type { EleProTable } from 'ele-admin-pro/es';
import MenuEdit from './components/menu-edit.vue';
import MenuPlug from './components/menu-plug.vue';
import Clone from './components/clone.vue';
import { listMenus, removeMenu } from '@/api/system/menu';
import type { Menu, MenuParam } from '@/api/system/menu/model';
import { copyText, openUrl } from '@/utils/common';
import { TOKEN_STORE_NAME } from '@/config/setting';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格列配置
const columns = ref<ColumnItem[]>([
{
key: 'index',
width: 48,
align: 'center',
fixed: 'left',
hideInSetting: true,
customRender: ({ index }) => index + (tableRef.value?.tableIndex ?? 0)
},
{
title: '菜单名称',
key: 'title',
showSorterTooltip: false,
ellipsis: true
},
{
title: '路由地址',
dataIndex: 'path',
key: 'path',
showSorterTooltip: false,
ellipsis: true
},
{
title: '组件路径',
dataIndex: 'component',
key: 'component',
showSorterTooltip: false,
ellipsis: true
},
{
title: '权限标识',
dataIndex: 'authority',
key: 'authority',
showSorterTooltip: false,
ellipsis: true
},
{
title: 'API节点',
dataIndex: 'modules'
},
{
title: '菜单编号',
dataIndex: 'menuId',
hideInTable: true
},
{
title: '排序',
dataIndex: 'sortNumber',
showSorterTooltip: false,
width: 90
},
{
title: '可见',
dataIndex: 'hide',
showSorterTooltip: false,
customRender: ({ text }) => ['是', '否'][text],
width: 90
},
{
title: '类型',
key: 'menuType',
showSorterTooltip: false,
width: 90
},
{
title: '创建时间',
dataIndex: 'createTime',
showSorterTooltip: false,
ellipsis: true,
hideInTable: true,
customRender: ({ text }) => toDateString(text)
},
{
title: '操作',
key: 'action',
width: 140,
align: 'center'
}
]);
// 当前编辑数据
const current = ref<Menu | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
const showPlug = ref(false);
const showClone = ref(false);
const token = ref(localStorage.getItem(TOKEN_STORE_NAME));
// 上级菜单id
const parentId = ref<number>();
// 菜单数据
const menuData = ref<Menu[]>([]);
// 表格展开的行
const expandedRowKeys = ref<number[]>([]);
// 表格数据源
const datasource: DatasourceFunction = ({ where }) => {
return listMenus({ ...where });
};
/* 数据转为树形结构 */
const parseData = (data: Menu[]) => {
return toTreeData({
data: data.map((d) => {
return { ...d, key: d.menuId, value: d.menuId };
}),
idField: 'menuId',
parentIdField: 'parentId'
});
};
/* 表格渲染完成回调 */
const onDone: EleProTableDone<Menu> = ({ data }) => {
menuData.value = data;
};
/* 刷新表格 */
const reload = (where?: MenuParam) => {
tableRef?.value?.reload({ where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: Menu | null, id?: number) => {
current.value = row ?? null;
parentId.value = id;
showEdit.value = true;
};
const addPlug = (row?: Menu) => {
current.value = row ?? null;
showPlug.value = true;
};
/* 一键克隆 */
const cloneMenu = (row?: Menu | null, id?: number) => {
current.value = row ?? null;
parentId.value = id;
showClone.value = true;
};
/* 删除单个 */
const remove = (row: Menu) => {
if (row.children?.length) {
message.error('请先删除子节点');
return;
}
const hide = messageLoading('请求中..', 0);
removeMenu(row.menuId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 展开全部 */
const expandAll = () => {
let keys: number[] = [];
eachTreeData(menuData.value, (d) => {
if (d.children && d.children.length && d.menuId) {
keys.push(d.menuId);
}
});
expandedRowKeys.value = keys;
};
/* 折叠全部 */
const foldAll = () => {
expandedRowKeys.value = [];
};
/* 自定义行属性 */
const customRow = (record: Menu) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
/* 点击展开图标时触发 */
const onExpand = (expanded: boolean, record: Menu) => {
if (expanded) {
expandedRowKeys.value = [
...expandedRowKeys.value,
record.menuId as number
];
} else {
expandedRowKeys.value = expandedRowKeys.value.filter(
(d) => d !== record.menuId
);
}
};
/* 判断是否是目录 */
const isDirectory = (d: Menu) => {
return !!d.children?.length && !d.component;
};
</script>
<script lang="ts">
import * as MenuIcons from '@/layout/menu-icons';
export default {
name: 'SystemMenu',
components: MenuIcons
};
</script>

View File

@@ -0,0 +1,171 @@
<!-- 节点编辑弹窗 -->
<template>
<ele-modal
:width="460"
:visible="visible"
:confirm-loading="loading"
:title="isUpdate ? '修改节点' : '添加节点'"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 5, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
>
<a-form-item label="名称" name="modules">
<a-input
allow-clear
:maxlength="50"
placeholder="shop"
v-model:value="form.modules"
/>
</a-form-item>
<a-form-item label="节点URL" name="modulesUrl">
<a-input
allow-clear
:maxlength="200"
placeholder="https://shop.wsdns.cn"
v-model:value="form.modulesUrl"
/>
</a-form-item>
<a-form-item label="备注">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入备注"
v-model:value="form.comments"
/>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { message } from 'ant-design-vue/es';
import type { FormInstance, Rule, RuleObject } from 'ant-design-vue/es/form';
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import useFormData from '@/utils/use-form-data';
import { addModules, updateModules } from '@/api/system/modules';
import type { Modules } from '@/api/system/modules/model';
import { isChinese, urlReg } from 'ele-admin-pro';
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Modules | null;
}>();
//
const formRef = ref<FormInstance | null>(null);
// 是否是修改
const isUpdate = ref(false);
// 提交状态
const loading = ref(false);
// 表单数据
const { form, resetFields, assignFields } = useFormData<Modules>({
id: undefined,
modules: '',
modulesUrl: '',
comments: undefined
});
// 表单验证规则
const rules = reactive<Record<string, Rule[]>>({
modules: [
{
required: true,
type: 'string',
trigger: 'blur',
validator: async (_rule: RuleObject, value: string) => {
if (!value) {
return Promise.reject('请输入节点ID');
} else if (isChinese(value)) {
return Promise.reject('仅支持英文字母');
} else {
return Promise.resolve();
}
}
}
],
modulesUrl: [
{
required: true,
message: '请输入正确的URL',
pattern: urlReg,
type: 'string',
trigger: 'blur'
}
]
});
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
if (isChinese(String(form.modules))) {
return message.error('节点名称请使用英文字母');
}
const saveOrUpdate = isUpdate.value ? updateModules : addModules;
saveOrUpdate(form)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
assignFields(props.data);
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
formRef.value?.clearValidate();
}
}
);
</script>

View File

@@ -0,0 +1,106 @@
<!-- 搜索表单 -->
<template>
<a-form
:label-col="
styleResponsive ? { xl: 7, lg: 5, md: 7, sm: 4 } : { flex: '90px' }
"
:wrapper-col="
styleResponsive ? { xl: 17, lg: 19, md: 17, sm: 20 } : { flex: '1' }
"
>
<a-row :gutter="8">
<a-col
v-bind="
styleResponsive
? { xl: 6, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 6 }
"
>
<a-form-item label="模块名称">
<a-input
v-model:value.trim="form.modules"
placeholder="请输入"
allow-clear
/>
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive
? { xl: 6, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 6 }
"
>
<a-form-item label="模块地址">
<a-input
v-model:value.trim="form.modulesUrl"
placeholder="请输入"
allow-clear
/>
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive
? { xl: 6, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 6 }
"
>
<a-form-item label="备注">
<a-input
v-model:value.trim="form.comments"
placeholder="请输入"
allow-clear
/>
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive
? { xl: 6, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 6 }
"
>
<a-form-item class="ele-text-right" :wrapper-col="{ span: 24 }">
<a-space>
<a-button type="primary" @click="search">查询</a-button>
<a-button @click="reset">重置</a-button>
</a-space>
</a-form-item>
</a-col>
</a-row>
</a-form>
</template>
<script lang="ts" setup>
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import useFormData from '@/utils/use-form-data';
import type { Modules, ModulesParam } from '@/api/system/modules/model';
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const emit = defineEmits<{
(e: 'search', where?: ModulesParam): void;
}>();
// 表单数据
const { form, resetFields } = useFormData<Modules>({
modules: '',
modulesUrl: '',
comments: ''
});
/* 搜索 */
const search = () => {
emit('search', form);
};
/* 重置 */
const reset = () => {
resetFields();
search();
};
</script>

View File

@@ -0,0 +1,160 @@
<template>
<div class="ele-body">
<a-card :bordered="false">
<!-- 表格 -->
<ele-pro-table
ref="tableRef"
row-key="id"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
:scroll="{ x: 800 }"
cache-key="proSystemModulesTable"
>
<template #toolbar>
<a-space>
<a-button type="primary" class="ele-btn-icon" @click="openEdit()">
<template #icon>
<plus-outlined />
</template>
<span>新建</span>
</a-button>
</a-space>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical" />
<a-popconfirm
placement="topRight"
title="确定要删除此模块吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<modules-edit v-model:visible="showEdit" :data="current" @done="reload" />
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { message } from 'ant-design-vue/es';
import { PlusOutlined } from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro/es';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import { messageLoading, toDateString } from 'ele-admin-pro/es';
import ModulesEdit from './components/modules-edit.vue';
import { pageModules, removeModules } from '@/api/system/modules';
import type { Modules, ModulesParam } from '@/api/system/modules/model';
import { Menu } from '@/api/system/menu/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格列配置
const columns = ref<ColumnItem[]>([
{
key: 'index',
width: 48,
align: 'center',
fixed: 'left',
hideInSetting: true,
customRender: ({ index }) => index + (tableRef.value?.tableIndex ?? 0)
},
{
title: '名称',
dataIndex: 'modules',
sorter: true,
showSorterTooltip: false
},
{
title: 'URL',
dataIndex: 'modulesUrl',
sorter: true,
showSorterTooltip: false
},
{
title: '备注',
dataIndex: 'comments',
sorter: true,
showSorterTooltip: false
},
{
title: '操作',
key: 'action',
width: 200,
align: 'center'
}
]);
// 表格选中数据
const selection = ref<Modules[]>([]);
// 当前编辑数据
const current = ref<Modules | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 表格数据源
const datasource: DatasourceFunction = ({ page, limit, where, orders }) => {
return pageModules({ ...where, ...orders, limit, page });
};
/* 搜索 */
const reload = (where?: ModulesParam) => {
selection.value = [];
tableRef?.value?.reload({ page: 1, where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: Modules) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 自定义行属性 */
const customRow = (record: Menu) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
/* 删除单个 */
const remove = (row: Modules) => {
const hide = messageLoading('请求中..', 0);
removeModules(row.id)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
</script>
<script lang="ts">
export default {
name: 'SystemModules'
};
</script>

View File

@@ -0,0 +1,131 @@
<!-- 详情弹窗 -->
<template>
<ele-modal
title="详情"
:width="640"
:footer="null"
:visible="visible"
@update:visible="updateVisible"
>
<a-form
class="ele-form-detail"
:label-col="{ sm: { span: 8 }, xs: { span: 6 } }"
:wrapper-col="{ sm: { span: 16 }, xs: { span: 18 } }"
>
<a-row :gutter="16">
<a-col :sm="12" :xs="24">
<a-form-item label="操作人">
<div class="ele-text-secondary">
{{ data.nickname }}({{ data.username }})
</div>
</a-form-item>
<a-form-item label="操作模块">
<div class="ele-text-secondary">
{{ data.module }}
</div>
</a-form-item>
<a-form-item label="操作时间">
<div class="ele-text-secondary">
{{ toDateString(data.createTime) }}
</div>
</a-form-item>
<a-form-item label="请求方式">
<div class="ele-text-secondary">
{{ data.requestMethod }}
</div>
</a-form-item>
</a-col>
<a-col :sm="12" :xs="24">
<a-form-item label="IP地址">
<div class="ele-text-secondary">
{{ data.ip }}
</div>
</a-form-item>
<a-form-item label="操作功能">
<div class="ele-text-secondary">
{{ data.description }}
</div>
</a-form-item>
<a-form-item label="请求耗时">
<div v-if="!isNaN(data.spendTime)" class="ele-text-secondary">
{{ data.spendTime / 1000 }}s
</div>
</a-form-item>
<a-form-item label="请求状态">
<a-tag :color="['green', 'red'][data.status]">
{{ ['正常', '异常'][data.status] }}
</a-tag>
</a-form-item>
</a-col>
</a-row>
<div style="margin: 12px 0">
<a-divider />
</div>
<a-form-item
label="请求地址"
:label-col="{ sm: { span: 4 }, xs: { span: 6 } }"
:wrapper-col="{ sm: { span: 20 }, xs: { span: 18 } }"
>
<div class="ele-text-secondary">
{{ data.url }}
</div>
</a-form-item>
<a-form-item
label="调用方法"
:label-col="{ sm: { span: 4 }, xs: { span: 6 } }"
:wrapper-col="{ sm: { span: 20 }, xs: { span: 18 } }"
>
<div class="ele-text-secondary">
{{ data.method }}
</div>
</a-form-item>
<a-form-item
label="请求参数"
:label-col="{ sm: { span: 4 }, xs: { span: 6 } }"
:wrapper-col="{ sm: { span: 20 }, xs: { span: 18 } }"
>
<div class="ele-text-secondary">
{{ data.params }}
</div>
</a-form-item>
<a-form-item
v-if="data.status === 0"
label="返回结果"
:label-col="{ sm: { span: 4 }, xs: { span: 6 } }"
:wrapper-col="{ sm: { span: 20 }, xs: { span: 18 } }"
>
<text-ellipsis :content="data.result" class="ele-text-secondary" />
</a-form-item>
<a-form-item
v-else
label="异常信息"
:label-col="{ sm: { span: 4 }, xs: { span: 6 } }"
:wrapper-col="{ sm: { span: 20 }, xs: { span: 18 } }"
>
<text-ellipsis :content="data.error" class="ele-text-secondary" />
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { toDateString } from 'ele-admin-pro/es';
import type { OperationRecord } from '@/api/system/operation-record/model';
import TextEllipsis from './text-ellipsis.vue';
const emit = defineEmits<{
(e: 'update:visible', visible: boolean): void;
}>();
defineProps<{
// 弹窗是否打开
visible?: boolean;
// 修改回显的数据
data: OperationRecord;
}>();
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
</script>

View File

@@ -0,0 +1,112 @@
<!-- 搜索表单 -->
<template>
<a-form
:label-col="
styleResponsive ? { xl: 7, lg: 5, md: 7, sm: 4 } : { flex: '90px' }
"
:wrapper-col="
styleResponsive ? { xl: 17, lg: 19, md: 17, sm: 20 } : { flex: '1' }
"
>
<a-row :gutter="8">
<a-col
v-bind="
styleResponsive
? { xl: 6, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 6 }
"
>
<a-form-item label="用户账号">
<a-input
v-model:value.trim="form.username"
placeholder="请输入"
allow-clear
/>
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive
? { xl: 6, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 6 }
"
>
<a-form-item label="操作模块">
<a-input
v-model:value.trim="form.module"
placeholder="请输入"
allow-clear
/>
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive
? { xl: 6, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 6 }
"
>
<a-form-item label="操作时间">
<a-range-picker
v-model:value="dateRange"
:show-time="true"
value-format="YYYY-MM-DD HH:mm:ss"
class="ele-fluid"
/>
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive
? { xl: 6, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 6 }
"
>
<a-form-item class="ele-text-right" :wrapper-col="{ span: 24 }">
<a-space>
<a-button type="primary" @click="search">查询</a-button>
<a-button @click="reset">重置</a-button>
</a-space>
</a-form-item>
</a-col>
</a-row>
</a-form>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import useFormData from '@/utils/use-form-data';
import type { OperationRecordParam } from '@/api/system/operation-record/model';
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const emit = defineEmits<{
(e: 'search', where?: OperationRecordParam): void;
}>();
// 表单数据
const { form, resetFields } = useFormData<OperationRecordParam>({
username: '',
module: ''
});
// 日期范围选择
const dateRange = ref<[string, string]>(['', '']);
/* 搜索 */
const search = () => {
const [createTimeStart, createTimeEnd] = dateRange.value;
emit('search', { ...form, createTimeStart, createTimeEnd });
};
/* 重置 */
const reset = () => {
resetFields();
dateRange.value = ['', ''];
search();
};
</script>

View File

@@ -0,0 +1,59 @@
<!-- 文本超出隐藏 -->
<template>
<div
:class="[
'demo-text-ellipsis ele-bg-white ele-border-split',
{ expanded: expanded }
]"
>
<div>{{ content }}</div>
<div
class="demo-text-ellipsis-footer ele-border-split ele-bg-white"
@click="expanded = !expanded"
>
<up-outlined v-if="expanded" />
<down-outlined v-else />
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { DownOutlined, UpOutlined } from '@ant-design/icons-vue';
defineProps<{
content?: string;
}>();
const expanded = ref(false);
</script>
<style lang="less" scoped>
.demo-text-ellipsis {
border-radius: 4px;
padding: 6px 12px 20px 12px;
position: relative;
border-width: 1px;
border-style: solid;
word-break: break-all;
&:not(.expanded) {
max-height: 192px;
overflow: hidden;
}
}
.demo-text-ellipsis-footer {
border-top-width: 1px;
border-top-style: solid;
position: absolute;
bottom: 0;
left: 0;
right: 0;
text-align: center;
font-size: 12px;
cursor: pointer;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
</style>

View File

@@ -0,0 +1,273 @@
<template>
<div class="ele-body">
<a-card :bordered="false">
<!-- 搜索表单 -->
<operation-record-search @search="reload" />
<!-- 表格 -->
<ele-pro-table
ref="tableRef"
row-key="id"
:columns="columns"
:datasource="datasource"
:scroll="{ x: 1000 }"
cache-key="proSystemOperationRecordTable"
>
<template #toolbar>
<a-space>
<a-button type="primary" class="ele-btn-icon" @click="exportData">
<template #icon>
<download-outlined />
</template>
<span>导出</span>
</a-button>
</a-space>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">正常</a-tag>
<a-tag v-else-if="record.status === 1" color="red">异常</a-tag>
</template>
<template v-else-if="column.key === 'action'">
<a @click="openDetail(record)">详情</a>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 详情弹窗 -->
<operation-record-detail v-model:visible="showInfo" :data="current" />
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { message } from 'ant-design-vue/es';
import { DownloadOutlined } from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro/es';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import { utils, writeFile } from 'xlsx';
import { messageLoading, toDateString } from 'ele-admin-pro/es';
import OperationRecordSearch from './components/operation-record-search.vue';
import OperationRecordDetail from './components/operation-record-detail.vue';
import {
pageOperationRecords,
listOperationRecords
} from '@/api/system/operation-record';
import type {
OperationRecord,
OperationRecordParam
} from '@/api/system/operation-record/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格列配置
const columns = ref<ColumnItem[]>([
{
key: 'index',
width: 80,
align: 'center',
fixed: 'left',
hideInSetting: true,
customRender: ({ index }) => index + (tableRef.value?.tableIndex ?? 0)
},
{
title: '账号',
dataIndex: 'username',
sorter: true,
showSorterTooltip: false,
ellipsis: true
},
{
title: '昵称',
dataIndex: 'nickname',
sorter: true,
showSorterTooltip: false,
ellipsis: true
},
{
title: '操作模块',
dataIndex: 'module',
sorter: true,
showSorterTooltip: false,
ellipsis: true
},
{
title: '操作功能',
dataIndex: 'description',
sorter: true,
showSorterTooltip: false,
ellipsis: true
},
{
title: '请求地址',
dataIndex: 'url',
sorter: true,
showSorterTooltip: false,
ellipsis: true
},
{
title: '请求方式',
dataIndex: 'requestMethod',
sorter: true,
showSorterTooltip: false,
width: 100,
align: 'center'
},
{
title: '状态',
key: 'status',
dataIndex: 'status',
sorter: true,
showSorterTooltip: false,
width: 100,
filters: [
{
text: '正常',
value: 0
},
{
text: '异常',
value: 1
}
],
filterMultiple: false,
align: 'center'
},
{
title: '耗时',
dataIndex: 'spendTime',
sorter: true,
showSorterTooltip: false,
width: 100,
customRender: ({ text }) => text / 1000 + 's'
},
{
title: '操作时间',
dataIndex: 'createTime',
sorter: true,
showSorterTooltip: false,
ellipsis: true,
customRender: ({ text }) => toDateString(text),
align: 'center'
},
{
title: '操作',
key: 'action',
width: 90,
align: 'center',
fixed: 'right'
}
]);
// 当前选中数据
const current = ref<OperationRecord>({
module: '',
description: '',
url: '',
requestMethod: '',
method: '',
params: '',
result: '',
error: '',
spendTime: 0,
os: '',
device: '',
browser: '',
ip: '',
status: 0,
createTime: '',
nickname: '',
username: ''
});
// 是否显示查看弹窗
const showInfo = ref(false);
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
return pageOperationRecords({
...where,
...orders,
...filters,
page,
limit
});
};
/* 刷新表格 */
const reload = (where?: OperationRecordParam) => {
tableRef?.value?.reload({ page: 1, where });
};
/* 详情 */
const openDetail = (row: OperationRecord) => {
current.value = row;
showInfo.value = true;
};
/* 导出数据 */
const exportData = () => {
const array = [
[
'账号',
'昵称',
'操作模块',
'操作功能',
'请求地址',
'请求方式',
'状态',
'耗时',
'操作时间'
]
];
// 请求查询全部(不分页)的接口
const hide = messageLoading('请求中..', 0);
tableRef.value?.doRequest(({ where, orders, filters }) => {
listOperationRecords({ ...where, ...orders, ...filters })
.then((data) => {
hide();
data.forEach((d) => {
array.push([
d.username,
d.nickname,
d.module,
d.description,
d.url,
d.requestMethod,
['正常', '异常'][d.status],
d.spendTime / 1000 + 's',
toDateString(d.createTime)
]);
});
writeFile(
{
SheetNames: ['Sheet1'],
Sheets: {
Sheet1: utils.aoa_to_sheet(array)
}
},
'操作日志.xlsx'
);
})
.catch((e) => {
hide();
message.error(e.message);
});
});
};
</script>
<script lang="ts">
export default {
name: 'SystemOperationRecord'
};
</script>

View File

@@ -0,0 +1,229 @@
<!-- 部门编辑弹窗 -->
<template>
<ele-modal
:width="620"
:visible="visible"
:confirm-loading="loading"
:title="isUpdate ? '修改部门' : '添加部门'"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 7, sm: 24 } : { flex: '90px' }"
:wrapper-col="styleResponsive ? { md: 17, sm: 24 } : { flex: '1' }"
>
<a-row :gutter="16">
<a-col
v-bind="styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 12 }"
>
<a-form-item label="上级部门" name="parentId">
<org-select
:data="organizationList"
placeholder="请选择上级部门"
v-model:value="form.parentId"
/>
</a-form-item>
<a-form-item label="部门简称" name="organizationName">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入部门名称"
v-model:value="form.organizationName"
/>
</a-form-item>
<a-form-item label="部门全称" name="organizationFullName">
<a-input
allow-clear
:maxlength="100"
placeholder="请输入部门全称"
v-model:value="form.organizationFullName"
/>
</a-form-item>
<a-form-item label="部门代码">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入部门代码"
v-model:value="form.organizationCode"
/>
</a-form-item>
</a-col>
<a-col
v-bind="styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 12 }"
>
<a-form-item label="类型" name="organizationType">
<org-type-select v-model:value="form.organizationType" />
</a-form-item>
<a-form-item label="排序号" name="sortNumber">
<a-input-number
:min="0"
:max="99999"
class="ele-fluid"
placeholder="请输入排序号"
v-model:value="form.sortNumber"
/>
</a-form-item>
<a-form-item label="备注">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入备注"
v-model:value="form.comments"
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { message } from 'ant-design-vue/es';
import type { FormInstance, Rule } from 'ant-design-vue/es/form';
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import useFormData from '@/utils/use-form-data';
import OrgSelect from './org-select.vue';
import OrgTypeSelect from './org-type-select.vue';
import {
addOrganization,
updateOrganization
} from '@/api/system/organization';
import type { Organization } from '@/api/system/organization/model';
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Organization | null;
// 部门id
organizationId?: number;
// 全部部门
organizationList: Organization[];
}>();
//
const formRef = ref<FormInstance | null>(null);
// 是否是修改
const isUpdate = ref(false);
// 提交状态
const loading = ref(false);
// 表单数据
const { form, resetFields, assignFields } = useFormData<Organization>({
organizationId: undefined,
parentId: undefined,
organizationName: '',
organizationFullName: '',
organizationCode: '',
organizationType: undefined,
sortNumber: undefined,
comments: ''
});
// 表单验证规则
const rules = reactive<Record<string, Rule[]>>({
organizationName: [
{
required: true,
message: '请输入部门名称',
type: 'string',
trigger: 'blur'
}
],
organizationFullName: [
{
required: true,
message: '请输入部门全称',
type: 'string',
trigger: 'blur'
}
],
organizationType: [
{
required: true,
message: '请选择部门类型',
type: 'string',
trigger: 'blur'
}
],
sortNumber: [
{
required: true,
message: '请输入排序号',
type: 'number',
trigger: 'blur'
}
]
});
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const orgData = {
...form,
parentId: form.parentId || 0
};
const saveOrUpdate = isUpdate.value
? updateOrganization
: addOrganization;
saveOrUpdate(orgData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
assignFields(props.data);
isUpdate.value = true;
} else {
form.parentId = props.organizationId;
isUpdate.value = false;
}
} else {
resetFields();
formRef.value?.clearValidate();
}
}
);
</script>

View File

@@ -0,0 +1,39 @@
<!-- 机构选择下拉框 -->
<template>
<a-tree-select
allow-clear
tree-default-expand-all
:placeholder="placeholder"
:value="value || undefined"
:tree-data="data"
:dropdown-style="{ maxHeight: '360px', overflow: 'auto' }"
@update:value="updateValue"
/>
</template>
<script lang="ts" setup>
import type { Organization } from '@/api/system/organization/model';
const emit = defineEmits<{
(e: 'update:value', value?: number): void;
}>();
withDefaults(
defineProps<{
// 选中的数据(v-modal)
value?: number;
// 提示信息
placeholder?: string;
// 机构数据
data: Organization[];
}>(),
{
placeholder: '请选择角色'
}
);
/* 更新选中数据 */
const updateValue = (value?: number) => {
emit('update:value', value);
};
</script>

View File

@@ -0,0 +1,39 @@
<!-- 机构类型选择下拉框 -->
<template>
<a-select
allow-clear
:value="value"
:placeholder="placeholder"
@update:value="updateValue"
>
<a-select-option v-for="item in data" :key="item.label" :value="item.value">
{{ item.label }}
</a-select-option>
</a-select>
</template>
<script lang="ts" setup>
import { getDictionaryOptions } from '@/utils/common';
const emit = defineEmits<{
(e: 'update:value', value: string): void;
}>();
withDefaults(
defineProps<{
value?: string;
placeholder?: string;
}>(),
{
placeholder: '请选择机构类型'
}
);
// 机构类型数据
const data = getDictionaryOptions('organizationType');
/* 更新选中数据 */
const updateValue = (value: string) => {
emit('update:value', value);
};
</script>

View File

@@ -0,0 +1,296 @@
<!-- 用户编辑弹窗 -->
<template>
<ele-modal
:width="800"
:visible="visible"
:confirm-loading="loading"
:title="isUpdate ? '修改用户' : '新建用户'"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 7, sm: 24 } : { flex: '90px' }"
:wrapper-col="styleResponsive ? { md: 17, sm: 24 } : { flex: '1' }"
>
<a-row :gutter="16">
<a-col
v-bind="styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 12 }"
>
<a-form-item label="所属部门">
<org-select
:data="organizationList"
placeholder="请选择所属部门"
v-model:value="form.organizationId"
/>
</a-form-item>
<a-form-item label="账号" name="username">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入用户账号"
v-model:value="form.username"
/>
</a-form-item>
<a-form-item label="姓名" name="realName">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入真实姓名"
v-model:value="form.realName"
/>
</a-form-item>
<a-form-item label="性别" name="sex">
<sex-select v-model:value="form.sex" />
</a-form-item>
<a-form-item label="角色" name="roles">
<role-select v-model:value="form.roles" />
</a-form-item>
</a-col>
<a-col
v-bind="styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 12 }"
>
<a-form-item label="手机号" name="phone">
<a-input
allow-clear
:maxlength="11"
placeholder="请输入手机号"
v-model:value="form.phone"
/>
</a-form-item>
<a-form-item label="邮箱" name="email">
<a-input
allow-clear
:maxlength="100"
placeholder="请输入邮箱"
v-model:value="form.email"
/>
</a-form-item>
<a-form-item label="出生日期">
<a-date-picker
class="ele-fluid"
value-format="YYYY-MM-DD"
placeholder="请选择出生日期"
v-model:value="form.birthday"
/>
</a-form-item>
<a-form-item v-if="!isUpdate" label="登录密码" name="password">
<a-input-password
:maxlength="20"
v-model:value="form.password"
placeholder="请输入登录密码"
/>
</a-form-item>
<a-form-item label="个人简介">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入个人简介"
v-model:value="form.introduction"
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { message } from 'ant-design-vue/es';
import type { FormInstance, Rule } from 'ant-design-vue/es/form';
import { emailReg, phoneReg } from 'ele-admin-pro/es';
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import useFormData from '@/utils/use-form-data';
import OrgSelect from './org-select.vue';
import RoleSelect from '../../user/components/role-select.vue';
import SexSelect from '../../user/components/sex-select.vue';
import { addUser, updateUser, checkExistence } from '@/api/system/user';
import type { User } from '@/api/system/user/model';
import type { Organization } from '@/api/system/organization/model';
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: User | null;
// 全部部门
organizationList: Organization[];
// 部门id
organizationId?: number;
}>();
//
const formRef = ref<FormInstance | null>(null);
// 是否是修改
const isUpdate = ref(false);
// 提交状态
const loading = ref(false);
// 表单数据
const { form, resetFields, assignFields } = useFormData<User>({
userId: undefined,
organizationId: undefined,
username: '',
nickname: '',
realName: '',
sex: undefined,
roles: [],
email: '',
phone: '',
password: '',
introduction: ''
});
// 表单验证规则
const rules = reactive<Record<string, Rule[]>>({
username: [
{
required: true,
type: 'string',
validator: (_rule: Rule, value: string) => {
return new Promise<void>((resolve, reject) => {
if (!value) {
return reject('请输入用户账号');
}
checkExistence('username', value, props.data?.userId)
.then(() => {
reject('账号已经存在');
})
.catch(() => {
resolve();
});
});
},
trigger: 'blur'
}
],
nickname: [
{
required: true,
message: '请输入昵称',
type: 'string',
trigger: 'blur'
}
],
realName: [
{
required: true,
message: '请输入真实姓名',
type: 'string',
trigger: 'blur'
}
],
// sex: [
// {
// required: true,
// message: '请选择性别',
// type: 'string',
// trigger: 'blur'
// }
// ],
roles: [
{
required: true,
message: '请选择角色',
type: 'array',
trigger: 'blur'
}
],
email: [
{
pattern: emailReg,
message: '邮箱格式不正确',
type: 'string',
trigger: 'blur'
}
],
password: [
{
required: true,
type: 'string',
validator: async (_rule: Rule, value: string) => {
if (isUpdate.value || /^[\S]{5,18}$/.test(value)) {
return Promise.resolve();
}
return Promise.reject('密码必须为5-18位非空白字符');
},
trigger: 'blur'
}
],
phone: [
{
required: true,
pattern: phoneReg,
message: '手机号格式不正确',
type: 'string',
trigger: 'blur'
}
]
});
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
form.nickname = form.realName;
form.alias = form.realName;
const saveOrUpdate = isUpdate.value ? updateUser : addUser;
saveOrUpdate(form)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
assignFields(props.data);
isUpdate.value = true;
} else {
form.organizationId = props.organizationId;
isUpdate.value = false;
}
} else {
resetFields();
formRef.value?.clearValidate();
}
}
);
</script>

View File

@@ -0,0 +1,231 @@
<template>
<!-- 表格 -->
<ele-pro-table
ref="tableRef"
row-key="userId"
:columns="columns"
:datasource="datasource"
height="calc(100vh - 290px)"
tool-class="ele-toolbar-form"
:scroll="{ x: 800 }"
tools-theme="default"
bordered
cache-key="proSystemOrgUserTable"
class="sys-org-table"
>
<template #toolbar>
<org-user-search @search="reload" @add="openEdit()" />
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'roles'">
<a-tag v-for="item in record.roles" :key="item.roleId" color="blue">
{{ item.roleName }}
</a-tag>
</template>
<template v-else-if="column.key === 'status'">
<a-switch
:checked="record.status === 0"
@change="(checked: boolean) => editStatus(checked, record)"
/>
</template>
<template v-if="column.key === 'balance'">
<span class="ele-text-success">
{{ formatNumber(record.balance) }}
</span>
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical" />
<a-popconfirm
placement="topRight"
title="确定要删除此用户吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
<!-- 编辑弹窗 -->
<org-user-edit
:data="current"
v-model:visible="showEdit"
:organization-list="organizationList"
:organization-id="organizationId"
@done="reload"
/>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { message } from 'ant-design-vue/es';
import type { EleProTable } from 'ele-admin-pro/es';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import { messageLoading } from 'ele-admin-pro/es';
import OrgUserSearch from './org-user-search.vue';
import OrgUserEdit from './org-user-edit.vue';
import { timeAgo } from 'ele-admin-pro';
import { formatNumber } from 'ele-admin-pro/es';
import { pageUsers, removeUser, updateUserStatus } from '@/api/system/user';
import type { User, UserParam } from '@/api/system/user/model';
import type { Organization } from '@/api/system/organization/model';
const props = defineProps<{
// 部门 id
organizationId?: number;
// 部门列表
organizationList: Organization[];
}>();
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '用户ID',
dataIndex: 'userId',
width: 120,
showSorterTooltip: false
},
{
title: '用户账号',
dataIndex: 'username',
sorter: true,
showSorterTooltip: false
},
{
title: '姓名',
dataIndex: 'realName',
sorter: true,
showSorterTooltip: false
},
{
title: '余额',
dataIndex: 'balance',
key: 'balance',
sorter: true,
showSorterTooltip: false
},
{
title: '性别',
dataIndex: 'sexName',
width: 80,
align: 'center',
showSorterTooltip: false,
hideInTable: true,
ellipsis: true
},
{
title: '角色',
key: 'roles'
},
{
title: '创建时间',
dataIndex: 'createTime',
sorter: true,
showSorterTooltip: false,
hideInTable: true,
ellipsis: true,
customRender: ({ text }) => timeAgo(text)
},
{
title: '状态',
key: 'status',
sorter: true,
showSorterTooltip: false,
width: 80,
align: 'center'
},
{
title: '操作',
key: 'action',
width: 100,
align: 'center'
}
]);
// 当前编辑数据
const current = ref<User | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 表格数据源
const datasource: DatasourceFunction = ({ page, limit, where, orders }) => {
if (props.organizationId) {
where.organizationId = props.organizationId;
}
return pageUsers({
...where,
...orders,
page,
limit
});
};
/* 搜索 */
const reload = (where?: UserParam) => {
tableRef?.value?.reload({ page: 1, where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: User) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 删除单个 */
const remove = (row: User) => {
const hide = messageLoading('请求中..', 0);
removeUser(row.userId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 修改用户状态 */
const editStatus = (checked: boolean, row: User) => {
const status = checked ? 0 : 1;
updateUserStatus(row.userId, status)
.then((msg) => {
row.status = status;
message.success(msg);
})
.catch((e) => {
message.error(e.message);
});
};
// 监听部门 id 变化
watch(
() => props.organizationId,
() => {
reload();
}
);
</script>
<style lang="less" scoped>
.sys-org-table :deep(.ant-table-body) {
overflow: auto !important;
overflow: overlay !important;
}
.sys-org-table :deep(.ant-table-pagination.ant-pagination) {
padding: 0 4px;
margin-bottom: 0;
}
</style>

View File

@@ -0,0 +1,83 @@
<!-- 搜索表单 -->
<template>
<a-row :gutter="16">
<a-col
v-bind="
styleResponsive ? { xl: 6, lg: 8, md: 12, sm: 24, xs: 24 } : { span: 6 }
"
>
<a-input
v-model:value.trim="form.keywords"
placeholder="请输入关键词"
allow-clear
/>
</a-col>
<!-- <a-col-->
<!-- v-bind="-->
<!-- styleResponsive ? { xl: 6, lg: 8, md: 12, sm: 24, xs: 24 } : { span: 6 }-->
<!-- "-->
<!-- >-->
<!-- <a-input-->
<!-- v-model:value.trim="form.nickname"-->
<!-- placeholder="请输入昵称"-->
<!-- allow-clear-->
<!-- />-->
<!-- </a-col>-->
<a-col
v-bind="
styleResponsive
? { xl: 12, lg: 8, md: 24, sm: 24, xs: 24 }
: { span: 12 }
"
>
<a-space :size="10" style="flex-wrap: wrap">
<a-button type="primary" class="ele-btn-icon" @click="search">
<template #icon>
<search-outlined />
</template>
<span>查询</span>
</a-button>
<a-button type="primary" class="ele-btn-icon" @click="add">
<template #icon>
<plus-outlined />
</template>
<span>新建</span>
</a-button>
</a-space>
</a-col>
</a-row>
</template>
<script lang="ts" setup>
import { PlusOutlined, SearchOutlined } from '@ant-design/icons-vue';
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import useFormData from '@/utils/use-form-data';
import type { UserParam } from '@/api/system/user/model';
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const emit = defineEmits<{
(e: 'search', where?: UserParam): void;
(e: 'add'): void;
}>();
// 表单数据
const { form } = useFormData<UserParam>({
keywords: '',
username: '',
nickname: ''
});
/* 搜索 */
const search = () => {
emit('search', form);
};
/* 添加 */
const add = () => {
emit('add');
};
</script>

View File

@@ -0,0 +1,71 @@
<!-- 角色选择下拉框 -->
<template>
<a-select
allow-clear
mode="multiple"
:value="roleIds"
:placeholder="placeholder"
@update:value="updateValue"
@blur="onBlur"
>
<a-select-option
v-for="item in data"
:key="item.roleId"
:value="item.roleId"
>
{{ item.roleName }}
</a-select-option>
</a-select>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import { message } from 'ant-design-vue/es';
import { listRoles } from '@/api/system/role';
import type { Role } from '@/api/system/role/model';
const emit = defineEmits<{
(e: 'update:value', value: Role[]): void;
(e: 'blur'): void;
}>();
const props = withDefaults(
defineProps<{
// 选中的角色
value?: Role[];
//
placeholder?: string;
}>(),
{
placeholder: '请选择角色'
}
);
// 选中的角色id
const roleIds = computed(() => props.value?.map((d) => d.roleId as number));
// 角色数据
const data = ref<Role[]>([]);
/* 更新选中数据 */
const updateValue = (value: number[]) => {
emit(
'update:value',
value.map((v) => ({ roleId: v }))
);
};
/* 获取角色数据 */
listRoles()
.then((list) => {
data.value = list;
})
.catch((e) => {
message.error(e.message);
});
/* 失去焦点 */
const onBlur = () => {
emit('blur');
};
</script>

View File

@@ -0,0 +1,45 @@
<!-- 角色选择下拉框 -->
<template>
<a-select
show-search
optionFilterProp="label"
:options="data"
allow-clear
:value="value"
:placeholder="placeholder"
@update:value="updateValue"
@blur="onBlur"
/>
</template>
<script lang="ts" setup>
import { getDictionaryOptions } from '@/utils/common';
const emit = defineEmits<{
(e: 'update:value', value: string): void;
(e: 'blur'): void;
}>();
withDefaults(
defineProps<{
value?: string;
placeholder?: string;
}>(),
{
placeholder: '请选择性别'
}
);
// 字典数据
const data = getDictionaryOptions('sex');
/* 更新选中数据 */
const updateValue = (value: string) => {
emit('update:value', value);
};
/* 失去焦点 */
const onBlur = () => {
emit('blur');
};
</script>

View File

@@ -0,0 +1,208 @@
<template>
<div class="ele-body">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-split-layout
width="266px"
allow-collapse
:right-style="{ overflow: 'hidden' }"
:style="{ minHeight: 'calc(100vh - 152px)' }"
>
<div>
<ele-toolbar theme="default">
<a-space :size="10">
<a-button type="primary" class="ele-btn-icon" @click="openEdit()">
<template #icon>
<plus-outlined />
</template>
<span>新建</span>
</a-button>
<a-button
type="primary"
:disabled="!current"
class="ele-btn-icon"
@click="openEdit(current)"
>
<template #icon>
<edit-outlined />
</template>
<span>修改</span>
</a-button>
<a-button
danger
type="primary"
:disabled="!current"
class="ele-btn-icon"
@click="remove"
>
<template #icon>
<delete-outlined />
</template>
<span>删除</span>
</a-button>
</a-space>
</ele-toolbar>
<div class="ele-border-split sys-organization-list">
<a-tree
:tree-data="(data as any)"
show-line
v-model:expanded-keys="expandedRowKeys"
v-model:selected-keys="selectedRowKeys"
@select="onTreeSelect"
/>
</div>
</div>
<template #content>
<org-user-list
v-if="current"
:organization-list="data"
:organization-id="current.organizationId"
/>
</template>
</ele-split-layout>
</a-card>
<!-- 编辑弹窗 -->
<org-edit
v-model:visible="showEdit"
:data="editData"
:organization-list="data"
:organization-id="current?.organizationId"
@done="query"
/>
</div>
</template>
<script lang="ts" setup>
import { createVNode, ref } from 'vue';
import { message, Modal } from 'ant-design-vue/es';
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
ExclamationCircleOutlined
} from '@ant-design/icons-vue';
import { messageLoading, toTreeData, eachTreeData } from 'ele-admin-pro/es';
import OrgUserList from './components/org-user-list.vue';
import OrgEdit from './components/org-edit.vue';
import {
listOrganizations,
removeOrganization
} from '@/api/system/organization';
import type { Organization } from '@/api/system/organization/model';
// 加载状态
const loading = ref(true);
// 树形数据
const data = ref<Organization[]>([]);
// 树展开的key
const expandedRowKeys = ref<number[]>([]);
// 树选中的key
const selectedRowKeys = ref<number[]>([]);
// 选中数据
const current = ref<Organization | null>(null);
// 是否显示表单弹窗
const showEdit = ref(false);
// 编辑回显数据
const editData = ref<Organization | null>(null);
/* 查询 */
const query = () => {
loading.value = true;
listOrganizations()
.then((list) => {
loading.value = false;
const eks: number[] = [];
list.forEach((d, i) => {
d.title = d.organizationName;
d.key = d.organizationId;
d.value = d.organizationId;
if (typeof d.key === 'number') {
eks.push(d.key);
}
});
expandedRowKeys.value = eks;
data.value = toTreeData({
data: list,
idField: 'organizationId',
parentIdField: 'parentId'
});
if (list.length) {
if (typeof list[0].key === 'number') {
selectedRowKeys.value = [list[0].key];
}
current.value = list[0];
current.value.organizationId = 0;
} else {
selectedRowKeys.value = [];
current.value = null;
}
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
};
/* 选择数据 */
const onTreeSelect = () => {
eachTreeData(data.value, (d) => {
if (typeof d.key === 'number' && selectedRowKeys.value.includes(d.key)) {
current.value = d;
return false;
}
});
};
/* 打开编辑弹窗 */
const openEdit = (item?: Organization | null) => {
editData.value = item ?? null;
showEdit.value = true;
};
/* 删除 */
const remove = () => {
Modal.confirm({
title: '提示',
content: '确定要删除选中的部门吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = messageLoading('请求中..', 0);
removeOrganization(current.value?.organizationId)
.then((msg) => {
hide();
message.success(msg);
query();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
query();
</script>
<script lang="ts">
export default {
name: 'SystemOrganization'
};
</script>
<style lang="less" scoped>
.sys-organization-list {
padding: 12px 6px;
height: calc(100vh - 242px);
border-width: 1px;
border-style: solid;
overflow: auto;
}
</style>

View File

@@ -0,0 +1,416 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="740"
:visible="visible"
:confirm-loading="loading"
:title="isUpdate ? '修改菜单' : '新建菜单'"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 6, sm: 4, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 18, sm: 20, xs: 24 } : { flex: '1' }
"
>
<a-row :gutter="16">
<a-col
v-bind="styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 12 }"
>
<a-form-item label="上级菜单" name="parentId">
<a-tree-select
allow-clear
:tree-data="menuList"
tree-default-expand-all
placeholder="请选择上级菜单"
:value="form.parentId || undefined"
:dropdown-style="{ maxHeight: '360px', overflow: 'auto' }"
@update:value="(value?: number) => (form.parentId = value)"
/>
</a-form-item>
<a-form-item label="菜单名称" name="title">
<a-input
allow-clear
placeholder="请输入菜单名称"
v-model:value="form.title"
@pressEnter="save"
/>
</a-form-item>
</a-col>
<a-col
v-bind="styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 12 }"
>
<a-form-item label="菜单类型" name="menuType">
<a-radio-group
v-model:value="form.menuType"
@change="onMenuTypeChange"
>
<a-radio :value="0">目录</a-radio>
<a-radio :value="1">菜单</a-radio>
<a-radio :value="2">按钮</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="打开方式">
<a-radio-group
v-model:value="form.openType"
:disabled="form.menuType === 0 || form.menuType === 2"
@change="onOpenTypeChange"
>
<a-radio :value="0">组件</a-radio>
<a-radio :value="1">内链</a-radio>
<a-radio :value="2">外链</a-radio>
</a-radio-group>
</a-form-item>
</a-col>
</a-row>
<div style="margin-bottom: 22px">
<a-divider />
</div>
<a-row :gutter="16">
<a-col
v-bind="styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 12 }"
>
<a-form-item label="菜单图标" name="icon">
<ele-icon-picker
:data="iconData"
:allow-search="false"
v-model:value="form.icon"
placeholder="请选择菜单图标"
:disabled="form.menuType === 2"
>
<template #icon="{ icon }">
<component :is="icon" />
</template>
</ele-icon-picker>
</a-form-item>
<a-form-item name="path">
<template #label>
<a-tooltip
v-if="form.openType === 2"
title="需要以`http://`、`https://`、`//`开头"
>
<question-circle-outlined
style="vertical-align: -2px; margin-right: 4px"
/>
</a-tooltip>
<span>{{ form.openType === 2 ? '外链地址' : '路由地址' }}</span>
</template>
<a-input
allow-clear
v-model:value="form.path"
:disabled="form.menuType === 2"
:placeholder="
form.openType === 2 ? '请输入外链地址' : '请输入路由地址'
"
/>
</a-form-item>
<a-form-item name="component">
<template #label>
<a-tooltip
v-if="form.openType === 1"
title="需要以`http://`、`https://`、`//`开头"
>
<question-circle-outlined
style="vertical-align: -2px; margin-right: 4px"
/>
</a-tooltip>
<span>{{ form.openType === 1 ? '内链地址' : '组件路径' }}</span>
</template>
<a-input
allow-clear
v-model:value="form.component"
:disabled="
form.menuType === 0 ||
form.menuType === 2 ||
form.openType === 2
"
:placeholder="
form.openType === 1 ? '请输入内链地址' : '请输入组件路径'
"
/>
</a-form-item>
</a-col>
<a-col
v-bind="styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 12 }"
>
<a-form-item label="权限标识" name="authority">
<a-input
allow-clear
placeholder="请输入权限标识"
v-model:value="form.authority"
:disabled="
form.menuType === 0 ||
(form.menuType === 1 && form.openType === 2)
"
/>
</a-form-item>
<a-form-item label="排序号" name="sortNumber">
<a-input-number
:min="0"
:max="99999"
class="ele-fluid"
placeholder="请输入排序号"
v-model:value="form.sortNumber"
@pressEnter="save"
/>
</a-form-item>
<a-form-item label="是否展示">
<a-switch
checked-children=""
un-checked-children=""
:checked="form.hide === 0"
:disabled="form.menuType === 2"
@update:checked="updateHideValue"
/>
<a-tooltip
title="选择不展示只注册路由不展示在侧边栏, 比如添加页面应该选择不展示"
>
<question-circle-outlined
style="vertical-align: -4px; margin-left: 16px"
/>
</a-tooltip>
</a-form-item>
</a-col>
</a-row>
<a-form-item
label="路由元数据"
name="meta"
:label-col="
styleResponsive ? { md: 3, sm: 4, xs: 24 } : { flex: '90px' }
"
:wrapper-col="
styleResponsive ? { md: 21, sm: 20, xs: 24 } : { flex: '1' }
"
>
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入JSON格式的路由元数据"
v-model:value="form.meta"
/>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { message } from 'ant-design-vue/es';
import type { FormInstance, Rule } from 'ant-design-vue/es/form';
import { QuestionCircleOutlined } from '@ant-design/icons-vue';
import { isExternalLink } from 'ele-admin-pro/es';
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import useFormData from '@/utils/use-form-data';
import { addMenu, updateMenu } from '@/api/system/menu';
import type { Menu } from '@/api/system/menu/model';
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Menu | null;
// 上级菜单id
parentId?: number;
// 全部菜单数据
menuList: Menu[];
}>();
//
const formRef = ref<FormInstance | null>(null);
// 是否是修改
const isUpdate = ref(false);
// 提交状态
const loading = ref(false);
// 表单数据
const { form, resetFields, assignFields } = useFormData<Menu>({
menuId: undefined,
parentId: undefined,
title: '',
menuType: 0,
openType: 0,
icon: '',
path: '',
component: '',
authority: '',
sortNumber: undefined,
hide: 0,
meta: ''
});
// 表单验证规则
const rules = reactive<Record<string, Rule[]>>({
title: [
{
required: true,
message: '请输入菜单名称',
type: 'string',
trigger: 'blur'
}
],
sortNumber: [
{
required: true,
message: '请输入排序号',
type: 'number',
trigger: 'blur'
}
],
meta: [
{
type: 'string',
validator: async (_rule: Rule, value: string) => {
if (value) {
const msg = '请输入正确的JSON格式';
try {
const obj = JSON.parse(value);
if (typeof obj !== 'object' || obj === null) {
return Promise.reject(msg);
}
} catch (_e) {
return Promise.reject(msg);
}
}
return Promise.resolve();
},
trigger: 'blur'
}
]
});
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const menuForm = {
...form,
// menuType 对应的值与后端不一致在前端处理
menuType: form.menuType === 2 ? 1 : 0,
parentId: form.parentId || 0
};
const saveOrUpdate = isUpdate.value ? updateMenu : addMenu;
saveOrUpdate(menuForm)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
/* menuType选择改变 */
const onMenuTypeChange = () => {
if (form.menuType === 0) {
form.authority = '';
form.openType = 0;
form.component = '';
} else if (form.menuType === 1) {
if (form.openType === 2) {
form.authority = '';
}
} else {
form.openType = 0;
form.icon = '';
form.path = '';
form.component = '';
form.hide = 0;
}
};
/* openType选择改变 */
const onOpenTypeChange = () => {
if (form.openType === 2) {
form.component = '';
form.authority = '';
}
};
const updateHideValue = (value: boolean) => {
form.hide = value ? 0 : 1;
};
/* 判断是否是目录 */
const isDirectory = (d: Menu) => {
return !!d.children?.length && !d.component;
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
const isExternal = isExternalLink(props.data.path);
const isInner = isExternalLink(props.data.component);
// menuType 对应的值与后端不一致在前端处理
const menuType =
props.data.menuType === 1 ? 2 : isDirectory(props.data) ? 0 : 1;
assignFields({
...props.data,
menuType,
openType: isExternal ? 2 : isInner ? 1 : 0,
parentId:
props.data.parentId === 0 ? undefined : props.data.parentId
});
isUpdate.value = true;
} else {
form.parentId = props.parentId;
isUpdate.value = false;
}
} else {
resetFields();
formRef.value?.clearValidate();
}
}
);
</script>
<script lang="ts">
import * as icons from '@/layout/menu-icons';
export default {
components: icons,
data() {
return {
iconData: [
{
title: '已引入的图标',
icons: Object.keys(icons)
}
]
};
}
};
</script>

View File

@@ -0,0 +1,106 @@
<!-- 搜索表单 -->
<template>
<a-form
:label-col="
styleResponsive ? { xl: 7, lg: 5, md: 7, sm: 4 } : { flex: '90px' }
"
:wrapper-col="
styleResponsive ? { xl: 17, lg: 19, md: 17, sm: 20 } : { flex: '1' }
"
>
<a-row :gutter="8">
<a-col
v-bind="
styleResponsive
? { xl: 6, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 6 }
"
>
<a-form-item label="菜单名称">
<a-input
v-model:value.trim="form.title"
placeholder="请输入"
allow-clear
/>
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive
? { xl: 6, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 6 }
"
>
<a-form-item label="菜单地址">
<a-input
v-model:value.trim="form.path"
placeholder="请输入"
allow-clear
/>
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive
? { xl: 6, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 6 }
"
>
<a-form-item label="权限标识">
<a-input
v-model:value.trim="form.authority"
placeholder="请输入"
allow-clear
/>
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive
? { xl: 6, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 6 }
"
>
<a-form-item class="ele-text-right" :wrapper-col="{ span: 24 }">
<a-space>
<a-button type="primary" @click="search">查询</a-button>
<a-button @click="reset">重置</a-button>
</a-space>
</a-form-item>
</a-col>
</a-row>
</a-form>
</template>
<script lang="ts" setup>
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import useFormData from '@/utils/use-form-data';
import type { MenuParam } from '@/api/system/menu/model';
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const emit = defineEmits<{
(e: 'search', where?: MenuParam): void;
}>();
// 表单数据
const { form, resetFields } = useFormData<MenuParam>({
title: '',
path: '',
authority: ''
});
/* 搜索 */
const search = () => {
emit('search', form);
};
/* 重置 */
const reset = () => {
resetFields();
search();
};
</script>

View File

@@ -0,0 +1,253 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="780"
:visible="visible"
:confirm-loading="loading"
title="安装插件"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
okText="安装插件"
:footer="footer"
@ok="save"
>
<template v-if="!isSuccess">
<a-alert
:description="`安装成功后,插件出现在左侧菜单中,请在 【系统设置->菜单管理】修改插件的名称及顺序`"
type="success"
closable
show-icon
style="margin-bottom: 20px"
>
<template #icon><SmileOutlined /></template>
</a-alert>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="
styleResponsive ? { md: 6, sm: 4, xs: 24 } : { flex: '90px' }
"
:wrapper-col="
styleResponsive ? { md: 18, sm: 20, xs: 24 } : { flex: '1' }
"
>
<a-row :gutter="16">
<a-col
v-bind="styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 12 }"
>
<a-form-item label="插件名称" name="parentId">
<span class="ele-text-heading">{{ data.title }}</span>
</a-form-item>
<a-form-item label="价格" name="comments">
{{ data.price }}
</a-form-item>
</a-col>
<a-col
v-bind="styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 12 }"
>
<a-form-item label="插件ID" name="menuId">
<span class="ele-text-secondary">{{ data.menuId }}</span>
</a-form-item>
<a-form-item label="开发商" name="sortName">
<span class="ele-text-secondary">
{{ data.tenantId === 5 ? '官方' : data.shortName }}
</span>
</a-form-item>
</a-col>
<a-col
v-bind="styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 24 }"
>
<a-form-item label="插件介绍" name="comments">
<div class="ele-text-secondary" style="padding: 5px">
<byte-md-viewer :value="data.comments" :plugins="plugins" />
</div>
</a-form-item>
</a-col>
</a-row>
</a-form>
</template>
<a-result
status="success"
title="安装成功"
v-if="isSuccess"
sub-title="请在 系统设置->菜单管理修改插件的名称及顺序"
>
<template #extra>
<a-button key="console" type="primary" @click="reset(data.path)"
>立即前往</a-button
>
</template>
</a-result>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { message } from 'ant-design-vue/es';
import type { FormInstance } from 'ant-design-vue/es/form';
import { isExternalLink } from 'ele-admin-pro/es';
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import useFormData from '@/utils/use-form-data';
import type { Menu } from '@/api/system/menu/model';
import { Plug } from '@/api/system/plug/model';
import { SmileOutlined } from '@ant-design/icons-vue';
import gfm from '@bytemd/plugin-gfm';
import highlight from '@bytemd/plugin-highlight-ssr';
// // 插件的中文语言文件
import zh_HansGfm from '@bytemd/plugin-gfm/locales/zh_Hans.json';
import { installPlug } from '@/api/system/menu';
import { reloadPageTab } from '@/utils/page-tab-util';
import { useRouter } from 'vue-router';
const { push } = useRouter();
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Plug | null;
// 上级插件id
parentId?: number;
// 全部插件数据
menuList: Plug[];
}>();
// 是否是修改
const isUpdate = ref(false);
// 提交状态
const loading = ref(false);
const appid = ref(undefined);
const formRef = ref<FormInstance | null>(null);
const isSuccess = ref(false);
const footer = ref();
// 表单数据
const { form, resetFields, assignFields } = useFormData<Plug>({
plugId: undefined,
menuId: undefined,
parentId: undefined,
title: '',
menuType: 0,
openType: 0,
icon: '',
path: '',
component: '',
authority: '',
comments: '',
sortNumber: undefined,
hide: 0,
meta: '',
status: 10
});
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const menuForm = {
...form,
// menuType 对应的值与后端不一致在前端处理
menuType: form.menuType === 2 ? 1 : 0,
parentId: form.parentId || 0
};
console.log(menuForm);
installPlug(form.menuId)
.then((msg) => {
loading.value = false;
isSuccess.value = true;
footer.value = null;
// message.success(msg);
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 插件
const plugins = ref([
gfm({
locale: zh_HansGfm
}),
highlight()
]);
const reset = (url) => {
console.log(url);
push(url);
};
/* 判断是否是目录 */
const isDirectory = (d: Menu) => {
return !!d.children?.length && !d.component;
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
const isExternal = isExternalLink(props.data.path);
const isInner = isExternalLink(props.data.component);
// menuType 对应的值与后端不一致在前端处理
const menuType =
props.data.menuType === 1 ? 2 : isDirectory(props.data) ? 0 : 1;
assignFields({
...props.data,
menuType,
openType: isExternal ? 2 : isInner ? 1 : 0,
parentId:
props.data.parentId === 0 ? undefined : props.data.parentId
});
isUpdate.value = true;
} else {
form.parentId = props.parentId;
isUpdate.value = false;
}
} else {
resetFields();
formRef.value?.clearValidate();
}
}
);
</script>
<script lang="ts">
import * as icons from '@/layout/menu-icons';
export default {
components: icons,
data() {
return {
iconData: [
{
title: '已引入的图标',
icons: Object.keys(icons)
}
]
};
}
};
</script>

View File

@@ -0,0 +1,67 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<!-- <a-radio-group :defaultValue="plugType" @change="handleTabs">-->
<!-- <a-radio-button :value="0">插件市场</a-radio-button>-->
<!-- <a-radio-button :value="10">我的插件</a-radio-button>-->
<!-- </a-radio-group>-->
<a-input-search
allow-clear
placeholder="请输入搜索关键词"
v-model:value="searchText"
@pressEnter="search"
@search="search"
/>
</a-space>
</template>
<script lang="ts" setup>
import useSearch from '@/utils/use-search';
import type { CustomerParam } from '@/api/oa/customer/model';
import { ref, watch } from 'vue';
import { AppParam } from '@/api/oa/app/model';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: CustomerParam): void;
(e: 'add'): void;
(e: 'remove'): void;
}>();
// 表单数据
const { where, resetFields } = useSearch<AppParam>({
appId: undefined,
userId: undefined,
keywords: undefined,
status: 0
});
// const plugType = ref<number>(0);
// 搜索内容
const searchText = ref(null);
/* 搜索 */
const search = () => {
resetFields();
if (searchText.value) {
where.keywords = searchText.value;
}
emit('search', where);
};
// const reload = () => {
// // 刷新当前路由
// emit('search', where);
// };
watch(
() => props.selection,
() => {}
);
</script>

View File

@@ -0,0 +1,213 @@
<template>
<a-card
style="background-color: transparent"
:body-style="{ padding: '0px' }"
>
<div class="card-title">
<a-typography-title :level="3">应用模块</a-typography-title>
</div>
<a-row :gutter="16">
<a-col
v-bind="
styleResponsive
? { xl: 6, lg: 6, md: 6, sm: 24, xs: 24 }
: { span: 12 }
"
class="gutter-row"
v-for="(item, index) in list"
:key="index"
:span="6"
>
<a-card class="gutter-box" hoverable>
<div class="plug-item">
<a-image
:height="80"
:width="80"
:preview="false"
:src="item.companyLogo"
@click="openUrl('/system/plug/detail?id=' + item.companyId)"
fallback="https://file.wsdns.cn/20230218/550e610d43334dd2a7f66d5b20bd58eb.svg"
/>
<div class="info">
<a
class="name ele-text-heading"
@click="openUrl('/system/plug/detail?id=' + item.companyId)"
>{{ item.tenantName }}</a
>
<a-rate class="rate" v-model:value="rate" disabled allow-half />
<div class="company ele-text-placeholder">
<a-typography-text
type="secondary"
:ellipsis="{ rows: 1, expandable: true, symbol: '...' }"
>
{{ item.companyName }}
</a-typography-text>
</div>
</div>
</div>
<div class="plug-desc ele-text-secondary">
<a-typography-paragraph
type="secondary"
:ellipsis="{ rows: 2, expandable: true, symbol: '显示' }"
:content="item.comments"
/>
</div>
<div class="plug-bottom">
<div class="downloads ele-text-placeholder"
>安装 {{ item.clicks }}</div
>
<a-button type="primary" disabled v-if="planId === item.tenantId"
>已安装</a-button
>
<a-button v-else type="primary" @click="onClone(item)"
>安装</a-button
>
</div>
</a-card>
<!-- <a-card class="gutter-box" hoverable>-->
<!-- <div class="flex-justify">-->
<!-- <div class="plug-item">-->
<!-- <a-image-->
<!-- :height="80"-->
<!-- :width="80"-->
<!-- :preview="false"-->
<!-- :src="item.companyLogo"-->
<!-- fallback="https://file.wsdns.cn/20230218/550e610d43334dd2a7f66d5b20bd58eb.svg"-->
<!-- />-->
<!-- <div class="info">-->
<!-- <span class="name ele-text-heading">{{ item.tenantName }}</span>-->
<!-- <a-rate class="rate" v-model:value="rate" disabled allow-half />-->
<!-- <div class="company ele-text-placeholder">-->
<!-- <a-typography-text-->
<!-- type="secondary"-->
<!-- :ellipsis="{ rows: 1, expandable: true, symbol: '..' }"-->
<!-- >-->
<!-- {{ item.companyName }}-->
<!-- </a-typography-text>-->
<!-- </div>-->
<!-- <div class="plug-desc ele-text-secondary">-->
<!-- <a-typography-paragraph-->
<!-- type="secondary"-->
<!-- :ellipsis="{ rows: 2, expandable: true, symbol: '显示' }"-->
<!-- :content="item.comments"-->
<!-- />-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<!-- <div class="plug-desc ele-text-secondary">-->
<!-- <a-button-->
<!-- @click="openUrl('/system/plug/detail?id=' + item.companyId)"-->
<!-- >去开通</a-button-->
<!-- >-->
<!-- </div>-->
<!-- </div>-->
<!-- </a-card>-->
</a-col>
</a-row>
</a-card>
</template>
<script setup lang="ts">
import { openUrl } from '@/utils/common';
import { onClone } from '@/utils/plug-uitl';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
import { ref } from 'vue';
import { Company, CompanyParam } from '@/api/system/company/model';
import useSearch from '@/utils/use-search';
import { message } from 'ant-design-vue/es';
import { pageCompanyAll } from '@/api/system/company';
const props = defineProps<{
// 修改回显的数据
use: boolean;
}>();
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const searchText = ref('');
const rate = ref(3.5);
const list = ref<Company[]>([]);
// 查询条件
const { where, resetFields } = useSearch<CompanyParam>({
keywords: undefined,
limit: 4,
recommend: true,
authoritative: 1
});
const reload = () => {
resetFields();
if (searchText.value) {
where.recommend = undefined;
where.keywords = searchText.value;
}
if (props.use) {
where.recommend = false;
}
where.sort = 'buys';
where.order = 'desc';
const hide = message.loading('加载中...');
pageCompanyAll(where)
.then((data) => {
if (data?.list) {
list.value = data?.list;
}
})
.finally(() => {
hide();
});
};
reload();
</script>
<style scoped lang="less">
.ele-body-card {
background-color: transparent;
padding: 20px;
}
.card-title {
display: flex;
justify-content: space-between;
margin-top: 10px;
}
.gutter-row {
margin: 0 auto 30px auto;
.gutter-box {
.plug-item {
display: flex;
.info {
font-size: 14px;
margin-left: 6px;
.name {
font-size: 20px;
display: -webkit-box;
overflow: hidden;
-webkit-box-orient: vertical;
text-overflow: ellipsis;
-webkit-line-clamp: 2;
font-weight: 500;
}
.rate {
font-size: 13px;
}
.company {
}
}
}
.plug-desc {
padding: 10px 0;
font-size: 16px;
min-height: 88px;
}
.plug-bottom {
display: flex;
justify-content: space-between;
}
}
}
</style>

View File

@@ -0,0 +1,254 @@
<template>
<a-card
style="background-color: transparent"
:bordered="false"
:body-style="{ padding: '0px' }"
>
<a-alert
message="欢迎使用"
description="请选择需要安装的镜像"
type="success"
show-icon
closable
/>
<div style="margin: 30px auto; display: flex; justify-content: center">
<a-space style="flex-wrap: wrap">
<industry-select
v-model:value="industry"
valueField="label"
size="large"
placeholder="按行业筛选"
class="ele-fluid"
@change="onIndustry"
/>
<a-input-search
allow-clear
size="large"
style="width: 360px"
placeholder="请输入搜索关键词"
v-model:value="where.keywords"
@pressEnter="reload"
@search="reload"
/>
<a-button size="large" @click="reset">重置</a-button>
</a-space>
</div>
<a-tabs v-model:active-key="where.sceneType" @change="onTabs">
<a-tab-pane tab="热门推荐" key="recommend" />
<a-tab-pane tab="免费热榜" key="free" />
<a-tab-pane tab="付费热榜" key="pay" />
<a-tab-pane tab="最新上架" key="new" />
<a-tab-pane tab="我的收藏" key="collect" />
</a-tabs>
<a-row :gutter="16">
<a-col
v-bind="
styleResponsive
? { xl: 6, lg: 6, md: 6, sm: 24, xs: 24 }
: { span: 12 }
"
class="gutter-row"
v-for="(item, index) in list"
:key="index"
:span="6"
>
<a-card hoverable>
<div class="gutter-box">
<div class="plug-item">
<a-image
:height="80"
:width="80"
:preview="false"
:src="item.companyLogo"
@click="openUrl('/system/plug/detail?id=' + item.companyId)"
fallback="https://file.wsdns.cn/20230218/550e610d43334dd2a7f66d5b20bd58eb.svg"
/>
<div class="info">
<a
class="name ele-text-heading"
@click="openUrl('/system/plug/detail?id=' + item.companyId)"
>{{ item.tenantName }}</a
>
<a-rate class="rate" v-model:value="rate" disabled allow-half />
<div class="company ele-text-placeholder">
<a-typography-text
type="secondary"
:ellipsis="{ rows: 1, expandable: true, symbol: '...' }"
>
{{ item.companyName }}
</a-typography-text>
</div>
</div>
</div>
<div class="plug-desc ele-text-secondary">
<a-typography-paragraph
type="secondary"
:ellipsis="{ rows: 2, expandable: true, symbol: '显示' }"
:content="item.comments"
/>
</div>
<div class="plug-bottom">
<div class="downloads ele-text-placeholder"
>安装 {{ item.clicks }}</div
>
<a-button type="primary" disabled v-if="planId === item.tenantId"
>已安装</a-button
>
<a-button v-else type="primary" @click="onClone(item)"
>安装</a-button
>
</div>
</div>
</a-card>
</a-col>
</a-row>
<div class="plug-page" v-if="list.length">
<a-pagination
v-model:current="where.page"
v-model:pageSize="where.limit"
size="large"
:total="total"
@change="reload"
/>
</div>
<a-empty v-else />
</a-card>
</template>
<script setup lang="ts">
import { openUrl } from '@/utils/common';
import { onClone } from '@/utils/plug-uitl';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
import { ref } from 'vue';
import { Company, CompanyParam } from '@/api/system/company/model';
import useSearch from '@/utils/use-search';
import { message } from 'ant-design-vue/es';
import { pageCompanyAll } from '@/api/system/company';
const props = defineProps<{
// 修改回显的数据
use: boolean;
}>();
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const searchText = ref('');
const rate = ref(3.5);
const list = ref<Company[]>([]);
const industry = ref<any>();
const total = ref<any>(0);
const planId = ref<number>(Number(localStorage.getItem('PlanId')));
// 查询条件
const { where, resetFields } = useSearch<CompanyParam>({
keywords: undefined,
industryParent: '',
industryChild: '',
recommend: undefined,
authoritative: 1,
sceneType: 'recommend',
limit: 20,
page: 1
});
const onIndustry = (item: any) => {
where.industryChild = item[1];
};
const onTabs = (index) => {
if (index == 'recommend') {
where.recommend = true;
}
if (index == 'free') {
where.recommend = false;
}
if (index == 'pay') {
where.recommend = false;
}
if (index == 'new') {
where.sceneType = 'new';
}
if (index == 'collect') {
where.sceneType = 'collect';
}
reload();
};
const reset = () => {
resetFields();
reload();
};
const reload = () => {
where.sort = 'buys';
where.order = 'desc';
pageCompanyAll(where).then((data) => {
total.value = data?.count;
if (data?.list) {
list.value = data?.list;
}
});
};
reload();
</script>
<style scoped lang="less">
.ele-body-card {
background-color: transparent;
padding: 20px;
}
.card-title {
display: flex;
justify-content: space-between;
margin-top: 10px;
}
.gutter-row {
margin: 0 auto 30px auto;
.gutter-box {
display: flex;
flex-direction: column;
justify-content: space-between;
min-height: 200px;
.plug-item {
display: flex;
.info {
font-size: 14px;
margin-left: 6px;
.name {
font-size: 20px;
display: -webkit-box;
overflow: hidden;
-webkit-box-orient: vertical;
text-overflow: ellipsis;
-webkit-line-clamp: 2;
font-weight: 500;
}
.rate {
font-size: 13px;
}
.company {
}
}
}
.plug-desc {
padding: 10px 0;
font-size: 16px;
}
.plug-bottom {
display: flex;
justify-content: space-between;
}
}
}
.ele-text-heading {
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
.plug-page {
display: flex;
justify-content: center;
}
</style>

View File

@@ -0,0 +1,186 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="540"
:visible="visible"
:confirm-loading="loading"
:title="`菜单克隆`"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 5, sm: 4, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 18, sm: 20, xs: 24 } : { flex: '1' }
"
>
<a-form-item label="选择模板" name="tenantId">
<a-input
placeholder="请输入要克隆的租户ID"
v-model:value="form.tenantId"
@pressEnter="save"
/>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { message } from 'ant-design-vue/es';
import type { FormInstance, Rule } from 'ant-design-vue/es/form';
import { isExternalLink } from 'ele-admin-pro/es';
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import useFormData from '@/utils/use-form-data';
import { clone } from '@/api/system/menu';
import type { Menu } from '@/api/system/menu/model';
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Menu | null;
// 上级菜单id
parentId?: number;
// 全部菜单数据
menuList: Menu[];
}>();
//
const formRef = ref<FormInstance | null>(null);
// 是否是修改
const isUpdate = ref(false);
// 提交状态
const loading = ref(false);
// 表单数据
const { form, resetFields, assignFields } = useFormData<Menu>({
title: '',
icon: '',
path: '',
component: '',
tenantId: undefined
});
// 表单验证规则
const rules = reactive<Record<string, Rule[]>>({
tenantId: [
{
required: true,
message: '克隆后原有的菜单将会抹除,请慎用!',
type: 'string',
trigger: 'blur'
}
]
});
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const menuForm = {
...form
};
clone(menuForm)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
/* 判断是否是目录 */
const isDirectory = (d: Menu) => {
return !!d.children?.length && !d.component;
};
// 查询租户列表
// const tenantList = ref<Tenant[]>([]);
// const reload = (tenantName?: any) => {
// listTenant({ tenantName }).then((result) => {
// tenantList.value = result;
// });
// };
//
// reload();
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
const isExternal = isExternalLink(props.data.path);
const isInner = isExternalLink(props.data.component);
// menuType 对应的值与后端不一致在前端处理
const menuType =
props.data.menuType === 1 ? 2 : isDirectory(props.data) ? 0 : 1;
assignFields({
...props.data,
menuType,
openType: isExternal ? 2 : isInner ? 1 : 0,
parentId:
props.data.parentId === 0 ? undefined : props.data.parentId
});
isUpdate.value = true;
} else {
form.parentId = props.parentId;
isUpdate.value = false;
}
} else {
resetFields();
formRef.value?.clearValidate();
}
}
);
</script>
<script lang="ts">
import * as icons from '@/layout/menu-icons';
export default {
components: icons,
data() {
return {
iconData: [
{
title: '已引入的图标',
icons: Object.keys(icons)
}
]
};
}
};
</script>

View File

@@ -0,0 +1,261 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="740"
:visible="visible"
:confirm-loading="loading"
:title="isUpdate ? '插件管理' : '发布插件'"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-alert
:description="`审核通过后,插件将展示在插件市场,可供其他用户安装和使用后,获取销售分成。`"
closable
show-icon
style="margin-bottom: 20px"
>
<template #icon><SmileOutlined /></template>
</a-alert>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 6, sm: 4, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 18, sm: 20, xs: 24 } : { flex: '1' }
"
>
<a-row :gutter="16">
<a-col
v-bind="styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 12 }"
>
<a-form-item label="模块名称" name="parentId">
<a-tree-select
allow-clear
:tree-data="menuList"
tree-default-expand-all
placeholder="请选择上级插件"
:value="form.parentId || undefined"
:dropdown-style="{ maxHeight: '360px', overflow: 'auto' }"
@update:value="(value?: number) => (form.parentId = value)"
/>
</a-form-item>
<a-form-item label="插件名称" name="title">
<a-input
allow-clear
placeholder="请输入插件名称"
v-model:value="form.title"
/>
</a-form-item>
<!-- <a-form-item label="关联应用" name="appId">-->
<!-- <SelectApp v-model:value="appId" @done="onApp" />-->
<!-- </a-form-item>-->
</a-col>
<a-col
v-bind="styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 12 }"
>
<a-form-item label="插件价格" name="price">
<a-input-number
allow-clear
:min="0"
:precision="2"
style="width: 200px"
placeholder="请输入插件价格"
v-model:value="form.price"
/>
<span class="ml-10"></span>
</a-form-item>
</a-col>
</a-row>
<div style="margin-bottom: 22px">
<a-divider />
</div>
<a-form-item
label="插件简介"
name="comments"
:label-col="
styleResponsive ? { md: 3, sm: 4, xs: 24 } : { flex: '90px' }
"
:wrapper-col="
styleResponsive ? { md: 21, sm: 20, xs: 24 } : { flex: '1' }
"
>
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入插件简介"
v-model:value="form.comments"
/>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { message } from 'ant-design-vue/es';
import type { FormInstance, Rule } from 'ant-design-vue/es/form';
import { isExternalLink } from 'ele-admin-pro/es';
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import useFormData from '@/utils/use-form-data';
import type { Menu } from '@/api/system/menu/model';
import { createPlug, updatePlug } from '@/api/system/plug';
import { Plug } from '@/api/system/plug/model';
import { SmileOutlined } from '@ant-design/icons-vue';
import { addDocs, updateDocs } from '@/api/cms/docs';
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Menu | null;
// 上级插件id
parentId?: number;
// 全部插件数据
menuList: Menu[];
}>();
// 是否是修改
const isUpdate = ref(false);
// 提交状态
const loading = ref(false);
const formRef = ref<FormInstance | null>(null);
// 表单数据
const { form, resetFields, assignFields } = useFormData<Plug>({
plugId: undefined,
menuId: undefined,
parentId: undefined,
title: '',
price: undefined,
comments: '',
status: undefined
});
// 表单验证规则
const rules = reactive<Record<string, Rule[]>>({
parentId: [
{
required: true,
message: '请选择模块',
type: 'number',
trigger: 'blur'
}
],
title: [
{
required: true,
message: '请输入插件名称',
type: 'string',
trigger: 'blur'
}
]
});
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form,
status: 10
// content: content.value
};
const saveOrUpdate = isUpdate.value ? updatePlug : createPlug;
saveOrUpdate(formData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
/* 判断是否是目录 */
const isDirectory = (d: Menu) => {
return !!d.children?.length && !d.component;
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
const isExternal = isExternalLink(props.data.path);
const isInner = isExternalLink(props.data.component);
// menuType 对应的值与后端不一致在前端处理
const menuType =
props.data.menuType === 1 ? 2 : isDirectory(props.data) ? 0 : 1;
assignFields({
...props.data,
menuType,
openType: isExternal ? 2 : isInner ? 1 : 0,
parentId:
props.data.parentId === 0 ? undefined : props.data.parentId
});
form.parentId = props.parentId;
isUpdate.value = true;
} else {
form.parentId = props.parentId;
isUpdate.value = false;
}
} else {
resetFields();
formRef.value?.clearValidate();
}
}
);
</script>
<script lang="ts">
import * as icons from '@/layout/menu-icons';
export default {
components: icons,
data() {
return {
iconData: [
{
title: '已引入的图标',
icons: Object.keys(icons)
}
]
};
}
};
</script>
<style lang="less">
.tab-pane {
min-height: 300px;
}
.ml-10 {
margin-left: 5px;
}
</style>

View File

@@ -0,0 +1,98 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-button type="primary" class="ele-btn-icon" @click="add">
<template #icon>
<PlusOutlined />
</template>
<span>发布插件</span>
</a-button>
<!-- <a-input-search-->
<!-- allow-clear-->
<!-- placeholder="请输入搜索关键词"-->
<!-- v-model:value="searchText"-->
<!-- @pressEnter="search"-->
<!-- @search="search"-->
<!-- />-->
</a-space>
</template>
<script lang="ts" setup>
import useSearch from '@/utils/use-search';
import type { CustomerParam } from '@/api/oa/customer/model';
import { computed, ref, watch } from 'vue';
import { AppParam } from '@/api/app/model';
import { useUserStore } from '@/store/modules/user';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: CustomerParam): void;
(e: 'add'): void;
(e: 'remove'): void;
}>();
// 表单数据
const { where, resetFields } = useSearch<AppParam>({
appId: undefined,
userId: undefined,
keywords: undefined,
status: 0
});
const plugType = ref<number>(0);
// 搜索内容
const searchText = ref(null);
const userId = ref(0);
/* 搜索 */
const search = () => {
resetFields();
if (searchText.value) {
where.keywords = searchText.value;
}
emit('search', where);
};
// 新增
const add = () => {
emit('add');
};
// 批量删除
const removeBatch = () => {
emit('remove');
};
const handleTabs = (e) => {
const index = Number(e.target.value);
const userStore = useUserStore();
const loginUser = computed(() => userStore.info ?? {});
plugType.value = index;
if (index > 0) {
userId.value = Number(loginUser.value.userId);
where.status = undefined;
where.userId = Number(loginUser.value.userId);
}
if (index == 0) {
where.userId = undefined;
where.status = 20;
}
search();
};
const reload = () => {
// 刷新当前路由
emit('search', where);
};
watch(
() => props.selection,
() => {}
);
</script>

View File

@@ -0,0 +1,296 @@
<template>
<div class="ele-body">
<a-card title="我的插件" :bordered="false">
<!-- 表格 -->
<ele-pro-table
ref="tableRef"
row-key="plugId"
:columns="columns"
:datasource="datasource"
:expand-icon-column-index="1"
:expanded-row-keys="expandedRowKeys"
cache-key="proSystemPlugTable"
@done="onDone"
@expand="onExpand"
>
<template #toolbar>
<PlugSearch
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'title'">
<div class="app-box">
<a-image
:height="50"
:width="50"
:preview="false"
:src="record.icon"
fallback="https://file.wsdns.cn/20230218/550e610d43334dd2a7f66d5b20bd58eb.svg"
/>
<!-- <component v-if="record.icon" :is="record.icon" />-->
<div class="app-info">
<a class="ele-text-heading" @click="openEdit(record)">
{{ record.title }}
</a>
<div class="ele-text-placeholder comments">
{{ record.comments }}
</div>
<a-space size="large" class="ele-text-placeholder">
<a
class="ele-text-placeholder"
:href="`${record.domain}`"
target="_blank"
>
{{ record.companyName }}
</a>
<span>下载: {{ record.clicks ? record.clicks : 0 }}</span>
<span>收藏: {{ record.installs ? record.installs : 0 }}</span>
</a-space>
</div>
</div>
</template>
<template v-else-if="column.key === 'comments'">
<span class="ele-text-secondary">
{{ record.comments }}
</span>
</template>
<template v-if="column.key === 'appType'">
<span class="ele-text-placeholder" v-if="record.appType === 'web'">
网站应用
</span>
<span
class="ele-text-placeholder"
v-if="record.appType === 'mp-weixin'"
>
小程序
</span>
<span
class="ele-text-placeholder"
v-if="record.appType === 'h5-weixin'"
>
公众号
</span>
<span
class="ele-text-placeholder"
v-if="record.appType === 'app-plus'"
>
移动应用
</span>
<span class="ele-text-placeholder" v-if="record.appType === 'plug'">
插件
</span>
</template>
<template v-if="column.key === 'price'">
<a class="ele-text-warning">{{ record.price }}</a>
</template>
<template v-if="column.key === 'shortName'">
<span class="ele-text-success">
{{ record.sortName }}
</span>
</template>
<template v-if="column.key === 'score'">
<a>{{ record.score.toFixed(1) }}</a>
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">正常</a-tag>
<a-tag v-if="record.status === 10" color="orange">待审核</a-tag>
<a-tag v-if="record.status === 20" color="green">已通过</a-tag>
<a-tag v-if="record.status === 30" color="red">已驳回</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">管理</a>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<PlugEdit
v-model:visible="showEdit"
:data="current"
:parent-id="parentId"
:menu-list="menuData"
@done="reload"
/>
<clone v-model:visible="showClone" @done="reload" />
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
const { push } = useRouter();
import type {
DatasourceFunction,
ColumnItem
// EleProTableDone
} from 'ele-admin-pro/es/ele-pro-table/types';
import PlugSearch from './components/plug-search.vue';
import { toTreeData, toDateString } from 'ele-admin-pro/es';
import type { EleProTable } from 'ele-admin-pro/es';
import PlugEdit from './components/plug-edit.vue';
import Clone from './components/clone.vue';
import { pagePlug } from '@/api/system/plug';
import type { Plug, PlugParam } from '@/api/system/plug/model';
import { Menu } from '@/api/system/menu/model';
import { listMenus } from '@/api/system/menu';
import { useRouter } from 'vue-router';
import { useUserStore } from '@/store/modules/user';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '插件ID',
dataIndex: 'menuId',
align: 'center'
},
{
title: '插件名称',
dataIndex: 'title',
key: 'title'
},
{
title: '价格',
dataIndex: 'price',
key: 'price',
align: 'center',
customRender: ({ text }) => '¥' + text
},
{
title: '评分',
dataIndex: 'score',
align: 'center',
key: 'score'
},
{
title: '审核状态',
dataIndex: 'status',
align: 'center',
key: 'status'
},
{
title: '操作',
key: 'action',
width: 200,
align: 'center'
}
]);
// 当前编辑数据
const current = ref<Plug | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
const showClone = ref(false);
// 上级菜单id
const parentId = ref<number>();
// 菜单数据
const menuData = ref<Menu[]>([]);
const userStore = useUserStore();
const loginUser = computed(() => userStore.info ?? {});
// 表格展开的行
const expandedRowKeys = ref<number[]>([]);
// 表格数据源
const datasource: DatasourceFunction = ({ where }) => {
where.userId = loginUser.value.userId;
return pagePlug({ ...where });
};
/* 表格渲染完成回调 */
// const onDone: EleProTableDone<Plug> = ({ data }) => {
// menuData.value = data;
// };
/* 刷新表格 */
const reload = (where?: PlugParam) => {
tableRef?.value?.reload({ where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: Plug | null, id?: number) => {
console.log(row);
current.value = row ?? null;
parentId.value = row?.menuId;
showEdit.value = true;
};
/* 一键克隆 */
const clonePlug = (row?: Plug | null, id?: number) => {
current.value = row ?? null;
parentId.value = id;
showClone.value = true;
};
const query = () => {
listMenus({}).then((res) => {
if (res) {
menuData.value = parseData(res);
}
});
};
/* 数据转为树形结构 */
const parseData = (data: Menu[]) => {
return toTreeData({
data: data
.filter((d) => d.menuType == 0)
.map((d) => {
if (d.parentId != 0) {
// d.disabled = true;
}
return { ...d, key: d.menuId, value: d.menuId };
}),
idField: 'menuId',
parentIdField: 'parentId'
});
};
query();
</script>
<script lang="ts">
import * as PlugIcons from '@/layout/menu-icons';
export default {
name: 'SystemPlug',
components: PlugIcons
};
</script>
<style lang="less" scoped>
.app-box {
display: flex;
.app-info {
display: flex;
margin-left: 6px;
margin-right: 6px;
flex-direction: column;
justify-content: space-between;
}
}
.cursor-pointer {
cursor: pointer;
}
.sys-org-table :deep(.ant-table-body) {
overflow: auto !important;
overflow: overlay !important;
}
.sys-org-table :deep(.ant-table-pagination.ant-pagination) {
padding: 0 4px;
margin-bottom: 0;
}
.ele-text-heading {
}
.comments {
width: 420px;
padding: 5px 0;
margin-bottom: 10px;
}
</style>

View File

@@ -0,0 +1,400 @@
<template>
<a-page-header :title="form.shortName" @back="push('/system/plug')">
<a-row :gutter="16">
<!-- 左侧区域-->
<a-col
v-bind="
styleResponsive
? { xl: 18, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 12 }
"
class="gutter-row"
:span="6"
>
<a-card :bordered="false" style="width: 100%; margin-bottom: 16px">
<div class="goods-info">
<div class="logo">
<a-image
:width="70"
:height="70"
:preview="false"
:src="form.companyLogo"
/>
</div>
<div class="info">
<a-card :bordered="false" :body-style="{ padding: 0 }">
<div class="goods-name ele-text-heading">{{
form.shortName
}}</div>
<div class="comments ele-text-secondary">{{
form.comments
}}</div>
</a-card>
<a-card :bordered="false" class="buy-card">
<div class="price-box">
<div class="left">
<div
><span class="ele-text-secondary">价格:</span
><span class="ele-text-danger price">¥50</span>/月</div
>
<div
><span class="ele-text-secondary">续费:</span
><span class="ele-text-heading">¥1280</span></div
>
</div>
<div class="right">
<div class="sales ele-text-secondary">浏览 35</div>
<div class="sales ele-text-secondary">评价 3</div>
</div>
</div>
</a-card>
</div>
</div>
<div class="goods-item">
<div class="title"> 套餐版本 </div>
<div class="info">
<a-radio-group v-model:value="plan" size="large">
<a-radio-button value="1">体验版(3用户)</a-radio-button>
<a-radio-button value="2">基础版(10用户)</a-radio-button>
<a-radio-button value="3">标准版(50用户)</a-radio-button>
<a-radio-button value="4">豪华版(100用户)</a-radio-button>
<a-radio-button value="5">旗舰版(不限)</a-radio-button>
</a-radio-group>
</div>
</div>
<div class="goods-item">
<div class="title"> 购买时长 </div>
<div class="info">
<a-radio-group v-model:value="duration" size="large">
<a-radio-button value="1">1个月</a-radio-button>
<a-radio-button value="2">1年</a-radio-button>
<a-radio-button value="3">2年</a-radio-button>
<a-radio-button value="4">3年</a-radio-button>
</a-radio-group>
</div>
</div>
<div class="goods-item">
<div class="title"></div>
<div class="info">
<a-button type="primary" size="large">立即购买</a-button>
</div>
</div>
</a-card>
<a-card
:bordered="false"
:body-style="{ padding: 0 }"
style="margin-bottom: 16px"
>
<div class="guarantee">
<span class="text ele-text-danger"
>服务保障平台购买支持5天无理由退款请勿线下支付</span
>
</div>
</a-card>
<a-card :body-style="{ padding: '0 16px' }">
<a-tabs v-model:active-key="active">
<a-tab-pane tab="商品详情" key="detail">
<Tenant :use="false" />
</a-tab-pane>
<a-tab-pane tab="商品价格" key="price">
<Tenant :use="true" />
</a-tab-pane>
<a-tab-pane tab="客户案例" key="case">
<Tenant :use="true" />
</a-tab-pane>
<a-tab-pane tab="使用指南" key="guide">
<Tenant :use="true" />
</a-tab-pane>
<a-tab-pane tab="用户评论(8)" key="comments">
<Tenant :use="true" />
</a-tab-pane>
</a-tabs>
</a-card>
</a-col>
<!-- 右侧区域 -->
<a-col
v-bind="
styleResponsive
? { xl: 6, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 12 }
"
class="gutter-row"
:span="6"
>
<a-card :bordered="false" class="task-card">
<a-list :bordered="false">
<a-list-item style="font-weight: 500">
服务厂商:{{ form.companyName }}
</a-list-item>
<a-list-item @click="openUrl('http://www.' + form.domain)">
官方网站:{{ form.domain }}
</a-list-item>
<a-list-item>
联系客服:<QqOutlined :style="{ fontSize: '18px' }" /><span
class="ele-text-secondary"
>
在线时间9:00到6:00</span
>
</a-list-item>
<a-list-item> 电话0771-5386339 </a-list-item>
<a-list-item> 邮箱:{{ form.email }} </a-list-item>
<a-list-item>
问题处理:<a-button @click="openUrl('/oa/task/add')"
>提交工单</a-button
></a-list-item
>
</a-list>
</a-card>
<a-card
title="建议搭配应用"
:bordered="false"
class="task-card"
:split="false"
:body-style="{ padding: '5px' }"
>
<a-row :gutter="16">
<a-col class="gutter-row" :span="12">
<a-button
type="link"
class="ele-text-secondary"
@click="openUrl('https://3x.antdv.com/components/overview-cn')"
>Ant Design Vue</a-button
>
</a-col>
<a-col class="gutter-row" :span="12">
<a-button
type="link"
class="ele-text-secondary"
@click="openUrl('https://eleadmin.com/doc/eleadminpro/')"
>EleAdmin Pro</a-button
>
</a-col>
<a-col class="gutter-row" :span="12">
<a-button
type="link"
class="ele-text-secondary"
@click="openUrl('https://eleadmin.com/doc/oauth2/')"
>后端教程Java</a-button
>
</a-col>
<a-col class="gutter-row" :span="12">
<a-button
type="link"
class="ele-text-secondary"
@click="openUrl('/system/plug')"
>插件扩展</a-button
>
</a-col>
<a-col class="gutter-row" :span="12">
<a-button
type="link"
class="ele-text-secondary"
@click="openUrl('http://git.gxwebsoft.com')"
>git仓库</a-button
>
</a-col>
</a-row>
</a-card>
</a-col>
</a-row>
</a-page-header>
</template>
<script lang="ts" setup>
import { reactive, ref, unref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { getFileSize, openUrl } from '@/utils/common';
import { setPageTabTitle } from '@/utils/page-tab-util';
import useFormData from '@/utils/use-form-data';
import { getCompanyAll } from '@/api/system/company';
import { Company } from '@/api/system/company/model';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
import { onClone } from '@/utils/plug-uitl';
const { push } = useRouter();
const ROUTE_PATH = '/system/plug/detail';
import { QqOutlined } from '@ant-design/icons-vue';
import Tenant from '@/views/system/plug/components/tenant.vue';
import Plug from '@/views/system/plug/components/plug.vue';
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const { currentRoute } = useRouter();
const logo = ref<any>([]);
const active = ref('detail');
const duration = ref<any>('1');
const plan = ref<any>('1');
const top = ref<number>(10);
// 用户信息
const { form, assignFields } = useFormData<Company>({
companyId: undefined,
companyName: undefined,
companyLogo: undefined,
shortName: undefined,
domain: undefined,
email: undefined,
tenantId: undefined,
tenantName: '',
comments: '',
version: undefined,
createTime: undefined
});
// 表单验证规则
const rules = reactive({
taskType: [
{
required: true,
message: '请选择工单类型',
type: 'string',
trigger: 'blur'
}
],
content: [
{
required: true,
type: 'string',
message: '请填写问题描述',
trigger: 'blur'
}
],
sortNumber: [
{
required: true,
message: '请输入排序号',
type: 'number',
trigger: 'blur'
}
]
});
// 查询租户信息
const query = () => {
const { query } = unref(currentRoute);
const id = query.id;
if (id) {
getCompanyAll(Number(id)).then((data) => {
assignFields({
...data
});
// 修改当前页签标题
setPageTabTitle(`${form.tenantName}`);
});
}
};
watch(
currentRoute,
(route) => {
const { path } = unref(route);
if (path !== ROUTE_PATH) {
return;
}
query();
},
{ immediate: true }
);
</script>
<script lang="ts">
export default {
name: 'SystemPlugDetail'
};
</script>
<style lang="less" scoped>
.goods-info {
max-width: 80%;
display: flex;
.logo {
display: flex;
justify-content: flex-end;
padding-right: 20px;
width: 120px;
}
.info {
.goods-name {
font-size: 22px;
}
.comments {
margin: 5px 0;
}
.buy-card {
margin-top: 20px;
width: 700px;
justify-content: space-between;
align-items: center;
background-color: #f3faee;
border-radius: 1px;
.price-box {
display: flex;
justify-content: space-between;
align-items: center;
.price {
font-size: 28px;
padding-right: 5px;
}
}
}
}
}
.goods-item {
max-width: 80%;
margin: 20px 0;
display: flex;
.title {
display: flex;
justify-content: flex-end;
align-items: center;
padding-right: 20px;
width: 120px;
}
}
.task-card {
padding: 2px !important;
margin-bottom: 16px;
}
.user-content {
max-width: 100%;
border-radius: 8px !important;
background-color: #a2ec71;
border: none;
}
.admin-content {
border-radius: 8px !important;
border: 3px solid #f1f1f1;
}
/deep/.markdown-body {
background-color: transparent; /* 设置背景透明 */
}
/deep/.markdown-body img {
}
.files {
margin-top: 10px;
}
#bottom {
margin-bottom: 20px;
}
.transparent-bg {
background-color: transparent; /* 设置背景透明 */
}
.item-name {
font-size: 14px;
}
.guarantee {
height: 82px;
background: url('https://oss.wsdns.cn/20231101/220819e37b2546b493f90a6de6e762f9.png?x-oss-process=image/resize,w_750/quality,Q_90');
background-repeat: no-repeat;
background-size: 80%;
background-color: #fbf0e5;
display: flex;
align-items: center;
.text {
padding: 0 180px;
font-size: 24px;
}
}
</style>

View File

@@ -0,0 +1,33 @@
<template>
<div class="">
<a-card
:bordered="false"
:style="{ backgroundColor: 'transparent' }"
:body-style="{ padding: '16px' }"
>
<Tenant :use="false" />
</a-card>
</div>
</template>
<script lang="ts" setup>
import Tenant from './components/tenant.vue';
// import Plug from './components/plug.vue';
// const active = ref('list');
</script>
<script lang="ts">
import * as PlugIcons from '@/layout/menu-icons';
export default {
name: 'SystemPlug',
components: PlugIcons
};
</script>
<style lang="less" scoped>
.ele-body-card {
background-color: transparent;
padding: 20px;
}
</style>

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