Merge branch 'dev' into dev-dictionary

# Conflicts:
#	package.json
#	src/store/menu.ts
#	src/utils/menu.ts
#	src/views/device/Instance/Detail/components/BindParentDevice/index.vue
#	src/views/system/Menu/Detail/BasicInfo.vue
#	src/views/system/Menu/index.vue
#	yarn.lock
This commit is contained in:
XieYongHong 2023-10-27 15:15:58 +08:00
commit 2abc3362c8
15 changed files with 367 additions and 226 deletions

View File

@ -25,7 +25,7 @@
"event-source-polyfill": "^1.0.31",
"global": "^4.4.0",
"jetlinks-store": "^0.0.3",
"jetlinks-ui-components": "^1.0.34-3",
"jetlinks-ui-components": "^1.0.34-4",
"js-cookie": "^3.0.1",
"jsencrypt": "^3.3.2",
"less": "^4.1.3",

View File

@ -25,9 +25,11 @@
<UserInfo />
</div>
</template>
<router-view v-slot="{ Component }">
<component :is="components || Component" />
</router-view>
<slot>
<router-view v-slot="{ Component }">
<component :is="components || Component" />
</router-view>
</slot>
</j-pro-layout>
</template>
@ -40,6 +42,7 @@ import { clearMenuItem } from 'jetlinks-ui-components/es/ProLayout/util';
import { AccountMenu } from '@/router/menu'
import { useSystem } from '@/store/system';
import { storeToRefs } from 'pinia';
import { useSlots } from 'vue'
type StateType = {
collapsed: boolean;
@ -55,7 +58,7 @@ const menu = useMenuStore();
const system = useSystem();
const {configInfo,layout, basicLayout} = storeToRefs(system);
const slots = useSlots()
const layoutConf = reactive({
theme: DefaultSetting.layout.theme,
siderWidth: layout.value.siderWidth,

1
src/hook/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './useRequest'

92
src/hook/useRequest.ts Normal file
View File

@ -0,0 +1,92 @@
import {onUnmounted, ref} from 'vue'
import type { Ref } from 'vue'
import {isFunction, get, isArray} from 'lodash-es'
import type { AxiosResponseRewrite } from '@/utils/request'
interface RequestOptions<T, S> {
immediate: boolean
/**
*
* @param data
* @returns
*/
onSuccess: (data: AxiosResponseRewrite<S>) => S | void
/**
*
* @returns
*/
formatName: string | [string]
onError: (e: any) => void
defaultParams: S | any | any[]
handleResponse: (data: any) => any
}
const defaultOptions: any = {
immediate: true,
formatName: 'result'
}
type Run = (...args: any[]) => void
export const useRequest = <T = any, S = any>(
request: (...args: any[]) => Promise<AxiosResponseRewrite<T>>,
options: Partial<RequestOptions<T, S>> = defaultOptions,
): {
data: Ref<S | undefined>,
loading: Ref<boolean>,
run: Run
} => {
const data = ref<S>()
const loading = ref(false)
const _options = {
...defaultOptions,
...options
}
async function run(...arg: any[]) {
if (request && isFunction(request)) {
loading.value = true
try {
// @ts-ignore
const resp = await request.apply(this, arg)
loading.value = false
if (resp?.success) {
const successData = await _options.onSuccess?.(resp)
data.value = successData || get(resp, _options.formatName!)
// console.log(data.value)
} else {
_options.onError?.(resp)
}
} catch (e) {
loading.value = false
_options.onError?.(e)
}
}
}
if (_options.immediate) { // 主动触发
if (_options.defaultParams) {
isArray(_options.defaultParams) ? run(..._options.defaultParams) : run(_options.defaultParams)
} else {
run()
}
}
onUnmounted(() => {
// 销毁时,撤销该请求
if (request && isFunction(request)) {
}
// request()
})
return {
data,
loading,
run
}
}

View File

@ -12,7 +12,7 @@ import router from '@/router'
import { onlyMessage } from '@/utils/comm'
import { AccountMenu, NotificationRecordCode, NotificationSubscriptionCode } from '@/router/menu'
import { USER_CENTER_MENU_CODE } from '@/utils/consts'
import { isNoCommunity } from "@/utils/utils";
import {isNoCommunity} from "@/utils/utils";
const defaultOwnParams = [
{
@ -139,4 +139,4 @@ export const useMenuStore = defineStore({
})
}
}
})
})

View File

@ -1,7 +1,7 @@
import { BlankLayoutPage, BasicLayoutPage, SinglePage } from 'components/Layout'
import { isNoCommunity } from '@/utils/utils'
import Iframe from '../views/iframe/index.vue'
import { h } from 'vue'
import { shallowRef, defineAsyncComponent, h } from 'vue'
const pagesComponent = import.meta.glob('../views/**/*.vue');
@ -156,192 +156,6 @@ const extraRouteObj = {
}
};
//
// const resolveComponent = (name: any) => {
// const importPage = pagesComponent[`../views/${name}/index.vue`];
// console.log(importPage)
// if (!importPage) {
// return undefined
// } else {
// const res = () => importPage()
// return res
// }
// //@ts-ignore
// }
//
// const findChildrenRoute = (code: string, url: string, routes: any[] = []): MenuItem[] => {
// if (extraRouteObj[code]) {
// const extraRoutes = extraRouteObj[code].children.map((route: MenuItem) => {
// return {
// url: `${url}/${route.code}`,
// code: `${code}/${route.code}`,
// name: route.name,
// isShow: false
// }
// })
// return [...routes, ...extraRoutes]
// }
// return routes
// }
//
// const findDetailRouteItem = (code: string, url: string): Partial<MenuItem> | null => {
// const detailComponent = resolveComponent(`${code}/Detail`)
// if (detailComponent) {
// return {
// url: `${url}/detail/:id`,
// code: `${code}/Detail`,
// component: detailComponent,
// name: '详情信息',
// isShow: false
// }
// }
// return null
// }
//
// const findDetailRoutes = (routes: any[]): any[] => {
// const newRoutes: any[] = []
// routes.forEach((route: any) => {
// newRoutes.push(route)
// const detail = findDetailRouteItem(route.code, route.url)
// if (detail) {
// newRoutes.push(detail)
// }
// })
// return newRoutes
// }
//
// const filterMenus = ['device/DashBoard']
// export const filterCommunityMenus = (menuData: any[]) => {
// return menuData.filter(item => {
// if (item.children) {
// item.children = filterCommunityMenus(item.children)
// }
// return !filterMenus.includes(item.code)
// })
// }
//
// export const findCodeRoute = (asyncRouterMap: any[]) => {
// const routeMeta = {}
//
// function getDetail(code: string, url: string) {
// const detail = findDetailRouteItem(code, url)
// if (!detail) return
//
// routeMeta[(detail as MenuItem).code] = {
// path: detail.url,
// title: detail.name,
// parentName: code,
// buttons: detail.buttons?.map((b: any) => b.id) || []
// }
// }
//
// function findChildren(data: any[], code: string = '') {
// data.forEach(route => {
// routeMeta[route.code] = {
// path: route.url || route.path,
// title: route.meta?.title || route.name,
// parentName: code,
// buttons: route.buttons?.map((b: any) => b.id) || []
// }
// const otherRoutes = extraRouteObj[route.code]
//
// if (otherRoutes) {
// otherRoutes.children.map((item: any) => {
// const _code = `${route.code}/${item.code}`
// const url = `${route.url}/${item.code}`
// routeMeta[_code] = {
// path: `${route.url}/${item.code}`,
// title: item.name,
// parentName: route.code,
// buttons: item.buttons?.map((b: any) => b.id) || []
// }
// getDetail(_code, url)
// })
// }
// const _code = route.appId ? `/${route.appId}${route.url}` : route.code
// if (!route.appId) {
// getDetail(_code, route.url)
// } else {
// routeMeta[_code] = {
// path: `/${route.appId}${route.url}`,
// title: route.name,
// parentName: code,
// buttons: []
// }
// }
// if (route.children) {
// findChildren(route.children, _code)
// }
// })
// }
//
// findChildren(asyncRouterMap)
//
// return routeMeta
// }
//
// export function filterAsyncRouter(asyncRouterMap: any, parentCode = '', level = 1): { menusData: any, silderMenus: any } {
// const _asyncRouterMap = cloneDeep(asyncRouterMap)
// const menusData: any[] = []
// const silderMenus: any[] = []
// _asyncRouterMap.forEach((route: any) => {
// const hasAppId = route.appId
// const _route: any = {
// path: hasAppId ? `/${route.appId}${route.url}` : `${route.url}`,
// name: hasAppId ? `/${route.appId}${route.url}` : route.code,
// url: hasAppId ? `/${route.appId}${route.url}` : route.url,
// meta: {
// icon: route.icon,
// title: route.name,
// hideInMenu: route.isShow === false,
// buttons: route.buttons?.map((b: any) => b.id) || [],
// isApp: hasAppId,
// },
// }
//
// const silder = { ..._route }
// // 查看是否有隐藏子路由
// route.children = findChildrenRoute(route.code, route.url, route.children)
// route.children = findDetailRoutes(route.children)
// if (route.children && route.children.length) {
// // TODO 查看是否具有详情页
// const { menusData: _menusData, silderMenus: _silderMenus } = filterAsyncRouter(route.children, `${parentCode}/${route.code}`, level + 1)
// _route.children = _menusData
// silder.children = _silderMenus
// const showChildren = _route.children.some((r: any) => !r.meta.hideInMenu)
//
// if (showChildren && _route.children.length) {
// _route.component = level === 1 ? BasicLayoutPage : BlankLayoutPage
// _route.redirect = _route.children[0].url
// } else {
// const myComponent = resolveComponent(route.code)
// // _route.component = myComponent ? myComponent : BlankLayoutPage;
// if (!!myComponent) {
// _route.component = myComponent;
// _route.children.map((r: any) => menusData.push(r))
// delete _route.children
// } else {
// _route.component = BlankLayoutPage
// }
// }
// } else {
// if (hasAppId) {
// _route.component = route.component || resolveComponent('iframe')
// } else {
// _route.component = route.component || resolveComponent(route.code) || BlankLayoutPage
// }
// }
// menusData.push(_route)
// silderMenus.push(silder)
// })
// return {
// menusData,
// silderMenus,
// }
// }
import { shallowRef } from 'vue'
type Buttons = Array<{ id: string }>
const hasAppID = (item: any): { isApp: boolean, appUrl: string } => {
@ -370,6 +184,12 @@ const handleMeta = (item: MenuItem, isApp: boolean) => {
const findComponents = (code: string, level: number, isApp: boolean, components: any) => {
const myComponents = components[code]
if (level === 1) { // BasicLayoutPage
if (myComponents) {
return h(BasicLayoutPage, {}, () => [h(defineAsyncComponent(() => myComponents()), {})])
}
if (isApp) {
return h(BasicLayoutPage, {}, () => [h(Iframe, {})])
}
return myComponents ? () => myComponents() : BasicLayoutPage
} else if (isApp){ // iframe
return Iframe
@ -536,4 +356,4 @@ export const handleAuthMenu = (menuData: any[], cb: (code: any, buttons: any[])
}
})
}
}
}

View File

@ -6,7 +6,7 @@ import { LoginPath } from '@/router/menu'
import { cleanToken, getToken, LocalStore } from '@/utils/comm'
import type { AxiosInstance, AxiosResponse } from 'axios'
interface AxiosResponseRewrite<T = any[]> extends AxiosResponse<T, any> {
export interface AxiosResponseRewrite<T = any[]> extends AxiosResponse<T, any> {
result: T
success: boolean
}

View File

@ -21,7 +21,6 @@
{{ TypeStringMap[data.record.valueType?.type] }}
</template>
<template #config="{ data }">
<!-- <OtherConfigInfo :value="data.record.valueType"></OtherConfigInfo>-->
<ConfigModal v-model:value="data.record.valueType" :showOther="false" />
</template>
</DataTableObject>
@ -54,7 +53,7 @@
<script setup lang="ts" name="MetadataDataType">
import { getUnit } from '@/api/device/instance';
import { ValueObject } from '../components'
import {DataType, ValueObject} from '../components'
import {
DataTableTypeSelect,
DataTableArray,
@ -68,7 +67,7 @@ import {
DataTableObject,
} from 'jetlinks-ui-components';
import { cloneDeep } from 'lodash-es';
import {handleTypeValue, typeSelectChange, TypeStringMap, useUnit} from '../columns'
import {handleTypeValue, typeSelectChange, TypeStringMap, useUnit, validatorConfig} from '../columns'
import ConfigModal from './ConfigModal.vue'
import { Form } from 'jetlinks-ui-components'
@ -137,7 +136,6 @@ const columns = [{
required: true,
rules: [{
validator(_: any, value: any) {
console.log('validator',value)
if (!value?.type) {
return Promise.reject('请选择数据类型')
}
@ -150,7 +148,23 @@ const columns = [{
{
title: '其他配置',
dataIndex: 'config',
width: 100
width: 100,
// components: {
// name: ConfigModal,
// props: {
// showOther: false
// }
// },
form: {
rules: [{
callback(rule: any, value: any, dataSource: any[]) {
const field = rule.field.split('.')
const fieldIndex = Number(field[1])
const values = dataSource.find((item, index) => index === fieldIndex)
return validatorConfig(values?.valueType)
}
}]
},
},
{
title: '操作',

View File

@ -30,19 +30,29 @@ const handle = async (appId: string, url: string) => {
const res = await getAppInfo_api(appId);
let menuUrl: any = url;
if (res.status === 200) {
if (res.result.page.routeType === 'hash') {
menuUrl = `#${url}`;
const result = res.result
if (result.page.routeType === 'hash') {
menuUrl = url.startsWith('/') ? `#${url}` : `#/${url}`;
}
if (res.result.provider === 'internal-standalone') {
const urlStandalone = `${res.result.page.baseUrl}/api/application/sso/${appId}/login?redirect=${menuUrl}?layout=false`;
if (result.page.parameters) {
const params = new URLSearchParams()
result.page.parameters.forEach((item: any) => {
params.set(item.key,item.value)
})
menuUrl += `?${params.toString()}`
}
if (result.provider === 'internal-standalone') {
const urlStandalone = `${result.page.baseUrl}/api/application/sso/${appId}/login?redirect=${menuUrl}?layout=false`;
iframeUrl.value = urlStandalone;
} else if (res.result.provider === 'internal-integrated') {
} else if (result.provider === 'internal-integrated') {
const tokenUrl = `${
res.result.page.baseUrl
result.page.baseUrl
}/${menuUrl}?layout=false&X-Access-Token=${LocalStore.get(TOKEN_KEY)}`;
iframeUrl.value = tokenUrl;
} else {
const urlOther = `${res.result.page.baseUrl}/${menuUrl}`;
const urlOther = `${result.page.baseUrl}/${menuUrl}`;
iframeUrl.value = urlOther;
}
}

View File

@ -144,12 +144,7 @@
<RequestTable
v-model:value="form.data.page.parameters"
value-type="select"
:value-options="[
{ label: '用户ID', value: '用户ID' },
{ label: '用户名', value: '用户名' },
{ label: 'token', value: 'token' },
]"
value-type="input"
/>
</j-form-item>
</template>
@ -1397,12 +1392,20 @@
<div class="dialog">
<MenuDialog
v-if="dialog.visible"
v-if="dialog.visible && dialog.current.provider !== 'third-party'"
v-model:visible="dialog.visible"
:data="dialog.current"
:mode="routeQuery.id ? 'edit' : 'add'"
@refresh="menuStory.jumpPage('system/Apply')"
/>
<ThirdMenu
v-if="dialog.visible && dialog.current.provider === 'third-party'"
:data="dialog.current"
:mode="routeQuery.id ? 'edit' : 'add'"
mode="add"
@cancel="dialog.visible = false"
@ok="menuStory.jumpPage('system/Apply')"
/>
</div>
</div>
</template>
@ -1420,6 +1423,7 @@ import {
import FormLabel from './FormLabel.vue';
import RequestTable from './RequestTable.vue';
import MenuDialog from '../../componenets/MenuDialog.vue';
import ThirdMenu from '../../componenets/ThirdMenu.vue';
import { getImage, onlyMessage } from '@/utils/comm';
import type { formType, dictType, optionsType, applyType } from '../typing';
import { getRoleList_api } from '@/api/system/user';
@ -1573,7 +1577,7 @@ const getType = async () => {
typeOptions.value = arr;
}
}
onMounted(async () => {
await getType();
getRoleIdList();

View File

@ -0,0 +1,150 @@
<template>
<j-modal
:confirmLoading="loading"
class="edit-dialog-container"
title="集成菜单"
visible
width="600px"
@cancel="cancel"
@ok="handleOk"
>
<p>
当前集成菜单
</p>
<j-tree
v-if="menuTree.length"
v-model:checkedKeys="menuState.checkedMenu"
v-model:expandedKeys="menuState.expandedKeys"
:fieldNames="{ key: 'code', title: 'name' }"
:height="300"
:tree-data="menuTree"
checkable
/>
<j-empty
v-else
/>
</j-modal>
</template>
<script name="ThirdMenu" setup>
import {getMenuTree_api, queryOwnThree} from '@/api/system/menu';
import { onlyMessage } from '@/utils/comm';
import { useMenuStore } from '@/store/menu';
import { useRequest } from '@/hook'
import {filterTree, getCheckByTree} from "@/views/system/Apply/componenets/util";
import {
saveOwnerMenu_api,
updateApp_api,
} from '@/api/system/apply';
const props = defineProps({
mode: {
type: String,
default: 'add'
},
data: {
type: Object,
default: () => ({})
}
})
const menuStory = useMenuStore();
const loading = ref(false)
const emit = defineEmits(['ok', 'cancel']);
const menuState = reactive({
checkedMenu: [],
expandedKeys: [],
menuTree: ''
})
const menuTree = computed(() => {
try {
return JSON.parse(menuState.menuTree || '[]')
} catch (e) {
return []
}
})
useRequest(queryOwnThree,
{
defaultParams: { terms: [{ column: 'owner', termType: 'isnull', value: 0 }]},
onSuccess(res) {
menuState.menuTree = JSON.stringify(res.result)
return res
}
}
)
const { run } = useRequest(getMenuTree_api, {
immediate: false,
onSuccess(res) {
menuState.checkedMenu = getCheckByTree(res.result)
return res
}
}) //
const cancel = () => {
if (props.mode === 'add') {
menuStory.jumpPage('system/Apply/Save', {}, { id: props.data?.id })
}
emit('cancel')
}
const handleOk = async () => {
if (!menuState.checkedMenu.length) {
onlyMessage('请勾选配置菜单', 'warning')
return
}
const id = props.data.id
const cloneData = JSON.parse(menuState.menuTree)
const filterData = filterTree(cloneData, menuState.checkedMenu)
loading.value = true
try {
const resp = await saveOwnerMenu_api('iot', id, filterData)
await updateApp_api(id, {
...props.data,
integrationModes: props.data?.integrationModes?.map((item) => item?.value || item),
page: {
...props.data?.page,
configuration: {
checkedSystem: props.data?.page?.configuration?.checkedSystem
}
}
})
if (resp.success) {
//
onlyMessage('操作成功');
emit('ok')
}
loading.value = false
} catch (e) {
console.log(e)
loading.value = false
}
}
const getBindMenuData = () => {
const id = props.data.id
run({
terms: [
{
column: 'appId',
value: id,
},
],
})
}
if (props.data?.id) {
getBindMenuData()
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,27 @@
export const getCheckByTree = (data: any[]): string[] => {
let keys: string[] = []
if (data.length) {
data.forEach(item => {
if (item.children) {
keys = [...getCheckByTree(item.children), ...keys]
} else {
keys.push(item.code)
}
})
}
return keys
}
export const filterTree = (data: any[], ids: string[]) => {
return data?.filter(item => {
delete item.id
item.options = {show: true}
if (ids.includes(item.code)) {
return true
} else if (item.children) {
item.children = filterTree(item.children, ids)
return item.children.length >0
}
return false
}) || []
}

View File

@ -183,12 +183,19 @@
</div>
<div class="dialogs">
<MenuDialog
v-if="dialogVisible"
v-if="dialogVisible && current.provider !== 'third-party'"
v-model:visible="dialogVisible"
mode="edit"
:data="current"
@refresh="table.refresh"
/>
<ThirdMenu
v-if="dialogVisible && current.provider === 'third-party'"
:data="current"
mode="edit"
@cancel="dialogVisible = false"
@ok="() => { dialogVisible = false; table.refresh}"
/>
</div>
<Add v-if="visible" @close="visible = false" />
</page-container>
@ -197,6 +204,7 @@
<script setup lang="ts" name="Apply">
import PermissionButton from '@/components/PermissionButton/index.vue';
import MenuDialog from './componenets/MenuDialog.vue';
import ThirdMenu from './componenets/ThirdMenu.vue'
import {
getApplyList_api,
changeApplyStatus_api,

View File

@ -129,6 +129,20 @@
/>
</j-form-item>
</j-col>
<j-col :span="12">
<j-form-item
label="所属应用"
name="owner"
>
<j-select
v-model:value="form.data.owner"
:options="[{ label: 'Iot', value: 'iot' }]"
allowClear
placeholder="请选择所属应用"
style="width: 100%"
/>
</j-form-item>
</j-col>
</j-row>
</div>
@ -371,9 +385,8 @@ const form = reactive({
const accessSupportValue = form.data.accessSupport;
const params = {
...form.data,
options:{
show:true
},
owner: form.data?.owner ?? null,
options: { show: true },
accessSupport: {
value: accessSupportValue,
label:
@ -383,7 +396,6 @@ const form = reactive({
? '支持'
: '间接控制',
},
owner: 'iot',
};
api(params)
.then((resp: any) => {
@ -454,7 +466,7 @@ type assetType = {
&::before {
position: absolute;
top: 5px px;
top: 5px;
left: 0;
width: 4px;
height: calc(100% - 10px);

View File

@ -3738,10 +3738,10 @@ jetlinks-store@^0.0.3:
resolved "https://registry.npmjs.org/jetlinks-store/-/jetlinks-store-0.0.3.tgz"
integrity sha512-AZf/soh1hmmwjBZ00fr1emuMEydeReaI6IBTGByQYhTmK1Zd5pQAxC7WLek2snRAn/HHDgJfVz2hjditKThl6Q==
jetlinks-ui-components@^1.0.34-3:
version "1.0.34-3"
resolved "https://registry.npmjs.org/jetlinks-ui-components/-/jetlinks-ui-components-1.0.34-3.tgz#c4f8182ce7419db40ed6d937ab0a7040bfb1549a"
integrity sha512-9jzzGOIR5ICCBdCVYfQoWyZCIFcauLuvIF+WN/djR1glC4lsBdegN06HpzxcGMfsJlWODxzjRXEq+UDNqiL4zg==
jetlinks-ui-components@^1.0.34-4:
version "1.0.34-4"
resolved "https://registry.npmjs.org/jetlinks-ui-components/-/jetlinks-ui-components-1.0.34-4.tgz#92cfc6be685988385a489f3e924383c4831f3ed5"
integrity sha512-td+RgaBC5lQxRuDsHkCg9UEzCcSy4XikufnabVGz5IxU+UmXu+PJUjz2wo9vDe8sPSctyk/5jQU+N6GBPlp8JA==
dependencies:
"@vueuse/core" "^9.12.0"
"@vueuse/router" "^9.13.0"