Merge branch 'dev' of github.com:jetlinks/jetlinks-ui-vue into dev

This commit is contained in:
JiangQiming 2023-03-01 20:29:12 +08:00
commit 5a2e42ec8f
10 changed files with 529 additions and 72 deletions

View File

@ -1,3 +1,31 @@
import server from '@/utils/request'
export const getSsoBinds_api = (): any =>server.get(`/application/sso/me/bindings`)
// 获取登录用户信息
export const getMeInfo_api = () => server.get(`/user/detail`);
// 修改登录用户信息
export const updateMeInfo_api = (data:object) => server.put(`/user/detail`,data);
// 修改登录用户密码
export const updateMepsd_api = (data:object) => server.put(`/user/passwd`,data);
// 第三方账号解绑
export const unBind_api = (appId: string) => server.post(`/application/sso/${appId}/unbind/me`);
/**
*
* @param type
* @param name
*/
export const validateField_api = (type: 'username' | 'password', name: string) => server.post(`/user/${type}/_validate`,name,{
headers: {
'Content-Type': 'text/plain'
}
});
/**
*
* @param password
*/
export const checkOldPassword_api = (password:string) => server.post(`/user/me/password/_validate`,password,{
headers: {
'Content-Type': 'text/plain'
}
});

View File

@ -1,5 +1,6 @@
import { OperatorItem } from '@/components/FRuleEditor/Operator/typings'
import server from '@/utils/request'
import { DeviceMetadata, ProductItem, DepartmentItem } from '@/views/device/Product/typings'
import { DeviceMetadata, ProductItem, DepartmentItem, MetadataType } from '@/views/device/Product/typings'
/**
*

View File

@ -0,0 +1,101 @@
<template>
<a-modal
visible
title="编辑"
@ok="handleOk"
width="770px"
@cancel="emits('update:visible', false)"
>
<a-form :model="form" layout="vertical">
<a-row :gutter="24">
<a-col :span="12">
<a-form-item
label="姓名"
:rules="[{ required: true, message: '姓名必填' }]"
>
<a-input
v-model:value="form.name"
placeholder="请输入姓名"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="用户名">
<a-input
v-model:value="form.username"
placeholder="请输入用户名"
disabled
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="24">
<a-col :span="12">
<a-form-item label="角色">
<a-input
:value="
form.roleList.map((item) => item.name).join(',')
"
placeholder="请输入角色"
disabled
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="组织">
<a-input
:value="
form.orgList.map((item) => item.name).join(',')
"
placeholder="请输入组织"
disabled
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="24">
<a-col :span="12">
<a-form-item label="手机号">
<a-input
v-model:value="form.telephone"
placeholder="请输入手机号"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="邮箱">
<a-input
v-model:value="form.email"
placeholder="请输入邮箱"
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-modal>
</template>
<script setup lang="ts">
import { updateMeInfo_api } from '@/api/account/center';
import { message } from 'ant-design-vue';
import { userInfoType } from '../typing';
const emits = defineEmits(['ok', 'update:visible']);
const props = defineProps<{
visible: boolean;
data: userInfoType;
}>();
const form = ref(props.data);
const handleOk = () => {
updateMeInfo_api(form.value).then((resp) => {
if (resp.status === 200) {
message.success('保存成功');
emits('ok');
emits('update:visible', false);
}
});
};
</script>
<style scoped></style>

View File

@ -0,0 +1,138 @@
<template>
<a-modal
visible
title="重置密码"
@ok="handleOk"
width="520px"
@cancel="emits('update:visible', false)"
>
<a-form :model="form" layout="vertical" ref="formRef">
<a-form-item
label="旧密码"
name="oldPassword"
:rules="[
{ required: true },
{ validator: checkMothods.old, trigger: 'blur' },
]"
>
<a-input
v-model:value="form.oldPassword"
placeholder="请输入旧密码"
/>
</a-form-item>
<a-form-item
label="密码"
name="newPassword"
:rules="[
{ required: true },
{ validator: checkMothods.new, trigger: 'blur' },
]"
>
<a-input-password
v-model:value="form.newPassword"
placeholder="请输入姓名"
/>
</a-form-item>
<a-form-item
label="确认密码"
name="confirmPassword"
:rules="[
{ required: true },
{ validator: checkMothods.confirm, trigger: 'blur' },
]"
>
<a-input-password
v-model:value="form.confirmPassword"
placeholder="请输入姓名"
/>
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup lang="ts">
import {
updateMepsd_api,
checkOldPassword_api,
validateField_api,
} from '@/api/account/center';
import { FormInstance, message } from 'ant-design-vue';
import { Rule } from 'ant-design-vue/lib/form';
const emits = defineEmits(['ok', 'update:visible']);
const props = defineProps<{
visible: boolean;
}>();
const formRef = ref<FormInstance>();
const form = ref<formType>({
oldPassword: '',
newPassword: '',
confirmPassword: '',
});
const checkMothods = {
old: async (_rule: Rule, value: string) => {
if (!value) return Promise.reject('请输入密码');
try {
const resp: any = await checkOldPassword_api(value);
if (resp.status === 200 && !resp.result.passed)
return Promise.reject(resp.result.reason);
else return Promise.resolve();
} catch (error) {
return Promise.reject('验证失败');
}
},
new: async (_rule: Rule, value: string) => {
if (!value) return Promise.reject('请输入密码');
else if (
form.value.confirmPassword &&
value !== form.value.confirmPassword
)
return Promise.reject('两次密码输入不一致');
try {
const resp: any = await validateField_api('password', value);
if (resp.status === 200 && !resp.result.passed)
return Promise.reject(resp.result.reason);
else return Promise.resolve();
} catch (error) {
return Promise.reject('验证失败');
}
},
confirm: async (_rule: Rule, value: string) => {
if (!value) return Promise.reject('请输入确认密码');
try {
const resp: any = await validateField_api('password', value);
if (resp.status === 200 && !resp.result.passed)
return Promise.reject(resp.result.reason);
else return Promise.resolve();
} catch (error) {
return Promise.reject('验证失败');
}
},
};
const handleOk = () => {
formRef.value?.validate().then(() => {
const params = {
oldPassword: form.value.oldPassword,
newPassword: form.value.newPassword,
};
// updateMepsd_api(params).then((resp) => {
// if (resp.status === 200) {
// message.success('');
// emits('ok');
// emits('update:visible', false);
// }
// });
});
};
console.clear();
type formType = {
oldPassword: string;
newPassword: string;
confirmPassword: string;
};
</script>
<style scoped></style>

View File

@ -63,11 +63,27 @@
</div>
<div class="info-card">
<p>角色</p>
<p>{{ userInfo.roleList.join(',') || '-' }}</p>
<p>
{{
(userInfo.roleList &&
userInfo.roleList
.map((item) => item.name)
.join(',')) ||
'-'
}}
</p>
</div>
<div class="info-card">
<p>组织</p>
<p>{{ userInfo.orgList.join(',') || '-' }}</p>
<p>
{{
(userInfo.orgList &&
userInfo.orgList
.map((item) => item.name)
.join(',')) ||
'-'
}}
</p>
</div>
<div class="info-card">
<p>邮箱</p>
@ -78,6 +94,7 @@
type="EditOutlined"
class="edit"
style="right: 40px"
@click="editInfoVisible = true"
/>
</div>
</div>
@ -94,10 +111,19 @@
>安全性高的密码可以使帐号更安全建议您定期更换密码,设置一个包含字母,符号或数字中至少两项且长度超过8位的密码</span
>
</div>
<AIcon type="EditOutlined" class="edit" />
<span class="edit">
<PermissionButton
:uhasPermission="`${permission}:update`"
type="link"
@click="editPasswordVisible = true"
>
<AIcon type="EditOutlined" style="color: #1d39c4;" />
</PermissionButton>
</span>
</div>
</div>
<div class="card">
<!-- 社区版不显示 -->
<div class="card" v-if="isNoCommunity">
<h3>绑定三方账号</h3>
<div class="content">
<div class="account-card" v-for="item in bindList">
@ -106,7 +132,7 @@
style="height: 50px"
alt=""
/>
<div class="text">
<Ellipsis style="width: 150px; font-size: 22px">
<div v-if="item.bound">
<div>绑定名{{ item.others.name }}</div>
<div>
@ -118,30 +144,99 @@
</div>
</div>
<div v-else>{{ item.name }}未绑定</div>
</div>
<a-button v-if="item.bound">解除绑定</a-button>
<a-button v-else type="primary">立即绑定</a-button>
</Ellipsis>
<a-popconfirm
v-if="item.bound"
title="确认解除绑定嘛?"
@confirm="() => unBind(item.id)"
>
<a-button>解除绑定</a-button>
</a-popconfirm>
<a-button
v-else
type="primary"
@click="clickBind(item.id)"
>立即绑定</a-button
>
</div>
</div>
</div>
<div class="card">
<!-- 第三方用户不显示 -->
<div class="card" v-if="!isApiUser">
<h3>首页视图</h3>
<div class="choose-view">
<a-row class="view-content" :gutter="24">
<a-col
:span="6"
class="select-item"
:class="{ selected: currentView === 'device' }"
@click="currentView = 'device'"
>
<img :src="getImage('/home/device.png')" alt="" />
</a-col>
<a-col
:span="6"
class="select-item"
:class="{ selected: currentView === 'ops' }"
@click="currentView = 'ops'"
>
<img :src="getImage('/home/ops.png')" alt="" />
</a-col>
<a-col
:span="6"
class="select-item"
:class="{
selected: currentView === 'comprehensive',
}"
@click="currentView = 'comprehensive'"
>
<img
:src="getImage('/home/comprehensive.png')"
alt=""
/>
</a-col>
</a-row>
<a-button type="primary" class="btn" @click="confirm"
>确定</a-button
>
</div>
</div>
<EditInfoDialog
v-if="editInfoVisible"
v-model:visible="editInfoVisible"
:data="userInfo"
@ok="getUserInfo"
/>
<EditPasswordDialog
v-if="editPasswordVisible"
v-model:visible="editPasswordVisible"
/>
</div>
</page-container>
</template>
<script setup lang="ts">
import PermissionButton from '@/components/PermissionButton/index.vue';
import EditInfoDialog from './components/EditInfoDialog.vue';
import EditPasswordDialog from './components/EditPasswordDialog.vue';
import { LockOutlined } from '@ant-design/icons-vue';
import { BASE_API_PATH, TOKEN_KEY } from '@/utils/variable';
import { LocalStore, getImage } from '@/utils/comm';
import { useUserInfo } from '@/store/userInfo';
import { message, UploadChangeParam, UploadFile } from 'ant-design-vue';
import { getSsoBinds_api } from '@/api/account/center';
import {
getMeInfo_api,
getSsoBinds_api,
unBind_api,
} from '@/api/account/center';
import moment from 'moment';
import { getMe_api, getView_api, setView_api } from '@/api/home';
import { isNoCommunity } from '@/utils/utils';
import { userInfoType } from './typing';
const userInfo = useUserInfo().$state.userInfos as any as userInfoType;
const permission = 'system/User';
const userInfo = ref<userInfoType>({});
//
const bindList = ref<any[]>([]);
const bindIcon = {
'dingtalk-ent-app': '/notice/dingtalk.png',
@ -149,6 +244,24 @@ const bindIcon = {
'internal-standalone': '/apply/provider1.png',
'third-party': '/apply/provider5.png',
};
const unBind = (id: string) => {
unBind_api(id).then((resp) => {
if (resp.status === 200) {
message.success('解绑成功');
getSsoBinds();
}
});
};
const clickBind = (id: string) => {
window.open(`/${origin}/application/sso/${id}/login?autoCreateUser=false`);
localStorage.setItem('onBind', 'false');
localStorage.setItem('onLogin', 'yes');
window.onstorage = (e) => {
if (e.newValue) {
getSsoBinds();
}
};
};
const upload = reactive({
fileList: [] as any[],
uploadLoading: false,
@ -158,36 +271,74 @@ const upload = reactive({
} else if (info.file.status === 'done') {
info.file.url = info.file.response?.result;
upload.uploadLoading = false;
userInfo.avatar = info.file.response?.result;
userInfo.value.avatar = info.file.response?.result;
} else if (info.file.status === 'error') {
console.log(info.file);
upload.uploadLoading = false;
message.error('logo上传失败请稍后再试');
}
},
});
//
const isApiUser = ref<boolean>();
const currentView = ref<string>('');
const confirm = () => {
setView_api({
name: 'view',
content: currentView.value,
}).then(() => message.success('保存成功'));
};
const editInfoVisible = ref<boolean>(false);
const editPasswordVisible = ref<boolean>(false);
init();
function init() {
getUserInfo();
isNoCommunity && getSsoBinds();
getViews();
}
/**
* 获取用户信息
*/
function getUserInfo() {
getMeInfo_api().then((resp) => {
userInfo.value = resp.result as userInfoType;
});
}
/**
* 获取绑定第三方账号
*/
function getSsoBinds() {
getSsoBinds_api().then((resp: any) => {
if (resp.status === 200) bindList.value = resp.result;
});
}
type userInfoType = {
avatar: string;
createTime: number;
email: string;
id: string;
name: string;
orgList: string[];
roleList: string[];
status: number;
telephone: string;
tenantDisabled: boolean;
type: { name: string; id: string };
username: string;
};
/**
* 获取首页视图
*/
function getViews() {
// api
getMe_api()
.then((resp: any) => {
if (resp && resp.status === 200) {
isApiUser.value = resp.result.dimensions.find(
(item: any) =>
item.type === 'api-client' ||
item.type.id === 'api-client',
);
if (!isApiUser.value) return getView_api();
}
})
.then((resp: any) => {
if (resp?.status === 200) {
if (resp.result) currentView.value = resp.result?.content;
else if (resp.result.username === 'admin') {
currentView.value = 'comprehensive';
} else currentView.value = 'init';
}
});
}
</script>
<style lang="less" scoped>
@ -216,6 +367,7 @@ type userInfoType = {
.content {
display: flex;
margin-top: 24px;
flex-wrap: wrap;
.content-item {
margin-right: 24px;
.info-card {
@ -250,16 +402,32 @@ type userInfoType = {
align-items: center;
justify-content: space-between;
padding: 24px;
}
}
.text {
display: -webkit-box;
font-size: 22px;
width: 150px;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
.choose-view {
width: 100%;
margin-top: 48px;
.view-content {
display: flex;
flex-flow: row wrap;
.select-item {
border: 2px solid transparent;
img {
width: 100%;
}
&.selected {
border-color: #10239e;
}
}
}
.btn {
display: block;
margin: 48px auto;
margin-bottom: 0;
}
}
}
}

17
src/views/account/Center/typing.d.ts vendored Normal file
View File

@ -0,0 +1,17 @@
import { dictType } from '@/views/system/Department/typing';
export type userInfoType = {
avatar: string;
createTime: number;
email: string;
id: string;
name: string;
orgList: dictType;
roleList: dictType;
status: number;
telephone: string;
tenantDisabled: boolean;
type: { name: string; id: string };
username: string;
};

View File

@ -5,7 +5,7 @@
<a-button :loading="save.loading" type="primary" @click="save.saveMetadata">保存</a-button>
</template>
<a-form ref="formRef" :model="form.model" layout="vertical">
<BaseForm :model-type="metadataStore.model.type" :type="type" v-model:value="form.model"></BaseForm>
<BaseForm :model-type="metadataStore.model.type" :type="type" v-model:value="form.model"></BaseForm>
</a-form>
</a-drawer>
</template>
@ -17,11 +17,11 @@ import { ProductItem } from '@/views/device/Product/typings';
import { message } from 'ant-design-vue/es';
import type { FormInstance } from 'ant-design-vue/es';
import { updateMetadata, asyncUpdateMetadata } from '../../metadata'
import { Store } from 'jetlinks-store';
import { detail } from '@/api/device/instance';
import { DeviceInstance } from '@/views/device/Instance/typings';
import BaseForm from './BaseForm.vue';
import { PropType } from 'vue';
import { _deploy } from '@/api/device/product';
const props = defineProps({
type: {
@ -104,10 +104,16 @@ const save = reactive({
setTimeout(() => window.close(), 300);
}
} else {
const { id } = route.params
if (props?.type === 'device') {
instanceStore.refresh(id as string)
} else {
productStore.refresh(id as string)
}
// Store.set(SystemConst.REFRESH_METADATA_TABLE, true);
if (deploy) {
// TODO
Store.set('product-deploy', deploy);
_deploy(id as string)
// Store.set('product-deploy', deploy);
} else {
save.resetMetadata();
message.success({

View File

@ -18,7 +18,6 @@
<a-tabs @change="handleConvertMetadata">
<a-tab-pane v-for="item in codecs" :key="item.id" :tab="item.name">
<div class="cat-panel">
<!-- TODO 代码编辑器 -->
<MonacoEditor v-model="value" theme="vs" style="height: 100%"></MonacoEditor>
</div>
</a-tab-pane>

View File

@ -43,7 +43,6 @@
</a-input>
</a-form-item>
<a-form-item label="物模型" v-bind="validateInfos.import" v-if="formModel.metadataType === 'script'">
<!-- TODO代码编辑器 -->
<MonacoEditor v-model="formModel.import" theme="vs" style="height: 300px"></MonacoEditor>
</a-form-item>
</a-form>
@ -57,8 +56,6 @@ import type { DefaultOptionType } from 'ant-design-vue/es/select';
import type { UploadProps, UploadFile, UploadChangeParam } from 'ant-design-vue/es';
import type { DeviceMetadata, ProductItem } from '@/views/device/Product/typings'
import { message } from 'ant-design-vue/es';
import { Store } from 'jetlinks-store';
import { SystemConst } from '@/utils/consts';
import { useInstanceStore } from '@/store/instance'
import { useProductStore } from '@/store/product';
import { FILE_UPLOAD } from '@/api/comm';
@ -139,11 +136,6 @@ const rules = reactive({
],
})
const { validate, validateInfos } = useForm(formModel, rules);
const onSubmit = () => {
validate().then(() => {
})
}
const fileList = ref<UploadFile[]>([])
const productList = ref<DefaultOptionType[]>([])
@ -208,11 +200,11 @@ const operateLimits = (mdata: DeviceMetadata) => {
const handleImport = async () => {
validate().then(async (data) => {
loading.value = true
const { id } = route.params || {}
if (data.metadata === 'alink') {
const res = await convertMetadata('from', 'alink', data.import)
if (res.status === 200) {
const metadata = JSON.stringify(operateLimits(res.result))
const { id } = route.params || {}
if (props?.type === 'device') {
await saveMetadata(id as string, metadata)
instanceStore.setCurrent(JSON.parse(metadata || '{}'))
@ -227,8 +219,13 @@ const handleImport = async () => {
loading.value = false
message.error('发生错误!')
}
Store.set(SystemConst.GET_METADATA, true)
Store.set(SystemConst.REFRESH_METADATA_TABLE, true)
if (props?.type === 'device') {
instanceStore.refresh(id as string)
} else {
productStore.refresh(id as string)
}
// Store.set(SystemConst.GET_METADATA, true)
// Store.set(SystemConst.REFRESH_METADATA_TABLE, true)
close()
} else {
try {
@ -255,21 +252,24 @@ const handleImport = async () => {
loading.value = false
if (resp.status === 200) {
if (props?.type === 'device') {
const metadata: DeviceMetadata = JSON.parse(paramsDevice || '{}')
// TODO
// MetadataAction.insert(metadata);
// instanceStore.setCurrent(metadata)
const detail = instanceStore.current
detail.metadata = paramsDevice
instanceStore.setCurrent(detail)
message.success('导入成功')
} else {
const metadata: ProductItem = JSON.parse(params?.metadata || '{}')
// TODO
// MetadataAction.insert(metadata);
// productStore.setCurrent(metadata)
const detail = productStore.current
detail.metadata = params.metadata
productStore.setCurrent(detail)
message.success('导入成功')
}
}
Store.set(SystemConst.GET_METADATA, true)
Store.set(SystemConst.REFRESH_METADATA_TABLE, true)
// Store.set(SystemConst.GET_METADATA, true)
// Store.set(SystemConst.REFRESH_METADATA_TABLE, true)
if (props?.type === 'device') {
instanceStore.refresh(id as string)
} else {
productStore.refresh(id as string)
}
close();
} catch (e) {
loading.value = false

View File

@ -23,8 +23,8 @@
:tooltip="{ title: '重置后将使用产品的物模型配置' }" key="reload">
重置操作
</PermissionButton>
<PermissionButton :uhasPermission="`${permission}:update`" @click="visible = true">快速导入</PermissionButton>
<PermissionButton :uhasPermission="`${permission}:update`" @click="cat = true">物模型TSL</PermissionButton>
<PermissionButton :hasPermission="`${permission}:update`" @click="visible = true">快速导入</PermissionButton>
<PermissionButton :hasPermission="`${permission}:update`" @click="cat = true">物模型TSL</PermissionButton>
</a-space>
</template>
@ -50,7 +50,6 @@
import PermissionButton from '@/components/PermissionButton/index.vue'
import { deleteMetadata } from '@/api/device/instance.js'
import { message } from 'ant-design-vue'
import { Store } from 'jetlinks-store'
import { SystemConst } from '@/utils/consts'
import { useInstanceStore } from '@/store/instance'
import Import from './Import/index.vue'
@ -75,11 +74,11 @@ const resetMetadata = async () => {
const resp = await deleteMetadata(id as string)
if (resp.status === 200) {
message.info('操作成功')
Store.set(SystemConst.REFRESH_DEVICE, true)
setTimeout(() => {
Store.set(SystemConst.REFRESH_METADATA_TABLE, true)
}, 400)
instanceStore.refresh(id as string)
// Store.set(SystemConst.REFRESH_DEVICE, true)
// setTimeout(() => {
// Store.set(SystemConst.REFRESH_METADATA_TABLE, true)
// }, 400)
}
}
</script>