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,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>

View File

@@ -0,0 +1,253 @@
<template>
<a-page-header
:title="title"
:sub-title="subTitle"
@back="() => $router.go(-1)"
>
<template #extra>
<a-tabs v-model:activeKey="activeKey" @change="onTabs">
<a-tab-pane key="free" tab="免费热门" />
<a-tab-pane key="pay" tab="付费热门" />
<a-tab-pane key="new" tab="最新上架" />
<a-tab-pane key="collect" tab="我的收藏" />
</a-tabs>
</template>
<template v-if="list.length > 0">
<a-row :gutter="16">
<a-col
v-bind="
styleResponsive
? { xl: 6, lg: 12, md: 12, 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"
style="margin-right: 10px"
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="value"
disabled
allow-half
/>
<div class="company ele-text-placeholder">
<a-typography-paragraph
type="secondary"
:ellipsis="{ rows: 2, expandable: true, symbol: '...' }"
>
{{ item.companyName }}
</a-typography-paragraph>
</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"
@click="() => openNotification('success', '开发中')"
>
安装 {{ 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-col>
</a-row>
</template>
</a-page-header>
</template>
<script lang="ts" setup>
import { ref, unref, watch } from 'vue';
import { pageCompanyAll } from '@/api/system/company';
import { Company, CompanyParam } from '@/api/system/company/model';
import { openUrl } from '@/utils/common';
import { notification } from 'ant-design-vue';
import useSearch from '@/utils/use-search';
import { useRouter } from 'vue-router';
import { setPageTabTitle } from '@/utils/page-tab-util';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
import { onClone } from '@/utils/plug-uitl';
import { message } from 'ant-design-vue/es';
const ROUTE_PATH = '/system/plug/list';
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const value = ref(3.5);
const title = ref('Tenant');
const subTitle = ref('租户系统');
const { currentRoute } = useRouter();
const list = ref<Company[]>([]);
const searchText = ref('');
const activeKey = ref('free');
const planId = ref<number>(Number(localStorage.getItem('PlanId')));
/**
* 通知提醒框
*/
const openNotification = (type: string, text: string) => {
notification[type]({
message: '通知提醒框',
description: text
});
};
// 查询条件
const { where, resetFields } = useSearch<CompanyParam>({
keywords: undefined,
limit: 500,
recommend: undefined,
authoritative: 1,
sort: 'buys',
order: 'desc'
});
const onTabs = () => {
resetFields();
if (activeKey.value == 'new') {
where.sort = 'createTime';
where.order = 'desc';
}
if (activeKey.value == 'free') {
where.sort = 'buys';
where.order = 'desc';
}
if (activeKey.value == 'pay') {
where.sort = 'likes';
where.order = 'desc';
}
if (activeKey.value == 'collect') {
where.sceneType = 'collect';
where.limit = 0;
}
reload();
};
const reload = () => {
if (searchText.value) {
where.keywords = searchText.value;
}
const hide = message.loading('加载中...');
pageCompanyAll(where)
.then((data) => {
if (data?.list) {
list.value = data.list;
}
})
.finally(() => {
hide();
});
};
reload();
watch(
currentRoute,
(route) => {
const { path } = unref(route);
if (path !== ROUTE_PATH) {
return;
}
const { query } = unref(currentRoute);
const { type } = query;
if (type == 'Tenant') {
title.value = 'Tenant';
subTitle.value = '租户系统';
setPageTabTitle('租户系统');
} else if (type == 'Vue') {
title.value = 'Vue';
subTitle.value = 'Vue开发的应用';
setPageTabTitle('Vue开发的应用');
} else if (type == 'UniApp') {
title.value = 'UniApp';
subTitle.value = '使用UniApp开发的移动应用';
setPageTabTitle('使用UniApp开发的移动应用');
} else if (type == 'WebSite') {
title.value = 'WebSite';
subTitle.value = '网站应用';
setPageTabTitle('网站应用');
}
},
{ immediate: true }
);
</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;
}
.gutter-row {
margin: 15px auto;
.gutter-box {
.plug-item {
display: flex;
.info {
font-size: 14px;
.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;
}
}
}
</style>

View File

@@ -0,0 +1,286 @@
<template>
<a-page-header :title="title" @back="() => $router.go(-1)">
<div
style="
display: flex;
margin-bottom: 50px;
flex-flow: column;
align-items: center;
"
>
<a-space style="flex-wrap: wrap">
<a-input-search
allow-clear
size="large"
style="width: 500px"
placeholder="请输入搜索关键词"
v-model:value="searchText"
@pressEnter="reload"
@search="reload"
/>
<a-button size="large" @click="reset">重置</a-button>
</a-space>
</div>
<div :bordered="false" class="ele-body-card">
<ele-split-layout
width="266px"
:right-style="{ overflow: 'hidden' }"
:style="{ minHeight: 'calc(100vh - 15px)' }"
>
<div class="ele-bg-white">
<ele-toolbar theme="default">
<div class="toolbar">
<span>应用分类</span>
</div>
</ele-toolbar>
<div class="ele-border-split sys-category-list"> </div>
</div>
<template #content>
<div v-if="list.length > 0">
<a-row :gutter="16">
<a-col
v-bind="styleResponsive ? { lg: 24 } : { span: 12 }"
class="gutter-row"
:span="6"
v-for="(item, index) in list"
:key="index"
>
<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="value"
disabled
allow-half
/>
<div class="company ele-text-placeholder">
<a-typography-paragraph
type="secondary"
:ellipsis="{
rows: 2,
expandable: true,
symbol: '...'
}"
>
{{ item.companyName }}
</a-typography-paragraph>
</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"
@click="() => openNotification('success', '开发中')"
>
安装 {{ 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-col>
</a-row>
</div>
<a-space
v-if="count > 0"
style="display: flex; justify-content: center"
>
<a-pagination
v-model:current="current"
:total="count"
@change="onChange"
/>
</a-space>
</template>
</ele-split-layout>
</div>
</a-page-header>
</template>
<script lang="ts" setup>
import { ref, unref, watch } from 'vue';
import { pageCompanyAll } from '@/api/system/company';
import { Company, CompanyParam } from '@/api/system/company/model';
import { openUrl } from '@/utils/common';
import { message } from 'ant-design-vue/es';
import { notification } from 'ant-design-vue';
import useSearch from '@/utils/use-search';
import { useRouter } from 'vue-router';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
import { onClone } from '@/utils/plug-uitl';
const ROUTE_PATH = '/system/plug/search';
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const value = ref(3.5);
const title = ref('搜索');
const { currentRoute } = useRouter();
const list = ref<Company[]>([]);
const searchText = ref('');
const current = ref(1);
const count = ref(0);
const planId = ref<number>(Number(localStorage.getItem('PlanId')));
/**
* 通知提醒框
*/
const openNotification = (type: string, text: string) => {
notification[type]({
message: '通知提醒框',
description: text
});
};
// 查询条件
const { where, resetFields } = useSearch<CompanyParam>({
keywords: undefined,
companyName: undefined,
limit: 10,
recommend: undefined,
authoritative: 1,
page: 1,
sort: 'buys',
order: 'desc'
});
const onChange = (page) => {
where.page = page;
reload();
};
const reset = () => {
searchText.value = '';
resetFields();
reload();
};
const reload = () => {
if (searchText.value) {
where.keywords = searchText.value;
}
pageCompanyAll(where).then((data) => {
if (data?.list) {
list.value = data.list;
count.value = data.count;
}
});
};
watch(
currentRoute,
(route) => {
const { path } = unref(route);
if (path !== ROUTE_PATH) {
return;
}
const { query } = unref(currentRoute);
const { type, keywords, companyName } = query;
if (companyName) {
where.companyName = String(companyName);
searchText.value = String(companyName);
}
if (keywords) {
searchText.value = String(keywords);
}
reload();
},
{ immediate: true }
);
</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: 0;
}
.gutter-row {
margin-bottom: 30px;
.gutter-box {
.plug-item {
display: flex;
.info {
margin-left: 10px;
font-size: 14px;
.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;
}
}
}
.sys-category-list {
padding: 12px 6px;
height: calc(100vh - 242px);
border-width: 1px;
border-style: solid;
overflow: auto;
}
.toolbar {
display: flex;
justify-content: space-between;
}
</style>