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