dc1a7e5e by lihua

门户首页

1 parent 7731293f
Showing 45 changed files with 5421 additions and 195 deletions
......@@ -101,6 +101,12 @@ VITE_APP_SERVICE_BASEURL = ms-daop-trust-api-service
#数字合约接口,身份认证有关的服务
VITE_APP_DIGITAL_CONTRACT_URL = ms-daop-trust-data-space-service
# 是否启用登录验证
VITE_verify = true
# 应用标识
VITE_appKey = '691689d8e4b0f359c04d204a'
# 本地访问地址
# VITE_API_CIRCULATION_URL = http://localhost:9000/circulation
......
......@@ -133,6 +133,9 @@ VITE_APP_CIRCULATION = http://192.168.6.20:18052/
#数据加工交付
VITE_APP_DATA_DELIVERY = http://192.168.6.20:38052/
# 是否启用登录验证
VITE_verify = true
# 是否在打包时生成 sourcemap
VITE_BUILD_SOURCEMAP = false
# 是否在打包时开启压缩,支持 gzip 和 brotli
......
......@@ -21,6 +21,7 @@ declare module '@vue/runtime-core' {
Dialog_form: typeof import('./src/components/Dialog/dialog_form.vue')['default']
Dialog_grid: typeof import('./src/components/Dialog/dialog_grid.vue')['default']
Dialog_pane: typeof import('./src/components/Dialog/dialog_pane.vue')['default']
DialogPlus: typeof import('./src/components/DialogPlus/src/DialogPlus.vue')['default']
Drawer: typeof import('./src/components/Drawer/index.vue')['default']
EchartsMap: typeof import('./src/components/EchartsMap/index.vue')['default']
Editor: typeof import('./src/components/Editor/src/Editor.vue')['default']
......@@ -28,13 +29,17 @@ declare module '@vue/runtime-core' {
FileUpload: typeof import('./src/components/FileUpload/index.vue')['default']
FixedActionBar: typeof import('./src/components/FixedActionBar/index.vue')['default']
Form: typeof import('./src/components/Form/index.vue')['default']
FormItem: typeof import('./src/components/FormItem/FormItem.vue')['default']
FormPlus: typeof import('./src/components/FormPlus/src/FormPlus.vue')['default']
GraphTopbar: typeof import('./src/components/RelationNetwork/graphTopbar.vue')['default']
Header: typeof import('./src/components/Header/index.vue')['default']
Hour: typeof import('./src/components/Schedule/component/hour.vue')['default']
ImagePreview: typeof import('./src/components/ImagePreview/index.vue')['default']
ImagesUpload: typeof import('./src/components/ImagesUpload/index.vue')['default']
ImageUpload: typeof import('./src/components/ImageUpload/index.vue')['default']
LineageGraph: typeof import('./src/components/LineageGraph/index.vue')['default']
ListPanel: typeof import('./src/components/ListPanel/index.vue')['default']
Logo: typeof import('./src/components/Logo/index.vue')['default']
LookBpmn: typeof import('./src/components/ApprovalProcess/src/components/LookBpmn.vue')['default']
Month: typeof import('./src/components/Schedule/component/month.vue')['default']
NotAllowed: typeof import('./src/components/NotAllowed/index.vue')['default']
......@@ -46,6 +51,7 @@ declare module '@vue/runtime-core' {
PcasCascader: typeof import('./src/components/PcasCascader/index.vue')['default']
Popover: typeof import('./src/components/Popover/index.vue')['default']
RelationNetwork: typeof import('./src/components/RelationNetwork/index.vue')['default']
Retrievepassword: typeof import('./src/components/Retrievepassword/index.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
Schedule: typeof import('./src/components/Schedule/index.vue')['default']
......
{
"appKey": "67bd3018e4b0cac8f9a5beeb"
}
......@@ -216,4 +216,4 @@ export const addorUpdateStaff = (param) => request({
url: `${import.meta.env.VITE_APP_PERSONAL_URL}/staff/save-or-update`,
method: 'put',
data: param
});
});
\ No newline at end of file
......
import request from "@/utils/request";
export const idaasLogin = (params) => request({
url: `/oauth/login`,
method: 'idaasPost',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data: params,
})
export const getLoginWebAuthn = () => request({
url: `/webauthn/assertion/options`,
method: 'idaasGet',
})
export const getPictureCode = () => request({
url: `/user/get-picture-code`,
method: 'get',
})
// 通用图形验证码
export const commoncheckImgCode = () => request({
url: `/ms-daop-user-service/user/validate-code/get`,
method: 'get',
responseType: 'blob'
})
// 校验图形验证码
export const checkImgCode = (params) => request({
url: `${import.meta.env.VITE_APP_AUTH_URL}/portal/check`,
method: 'post',
data: params
});
// 获取图形验证码
export const getImgCodeSrc = (params) => request({
url: `${import.meta.env.VITE_APP_AUTH_URL}/portal/get-captcha`,
method: 'get',
params
});
export const checkDeviceTypeRegist = (params) => request({
url: `/web-authn/check-device-type-regist?logonUser=${params.logonUser}&deviceType=${params.platform}`,
method: 'idaasGet'
});
/**
* 校验登录用户账号和密码
* @param logonUser 用户手机号
* @returns
*/
export const checkLoginUser = (logonUser:string) => request({
url: `/ms-daop-user-service/user/check-login-user-password?logonUser=${logonUser}`,
method: 'get'
});
export const registWebAuthn = () => request({
url: `/webauthn/attestation/options`,
method: 'idaasGet'
});
export const signUp = (data) => request({
url: `/web-authn/signup`,
method: 'idaasPost',
data: data
});
/** 发送登录验证码到手机 */
export const sendLoginCode = (mobileNo:string) => request({
url: `/ms-daop-user-service/user/get-login-sms-validate-code?mobileNo=${mobileNo}`,
method: 'get'
});
export const getWebAuth4jLogin = (data) => request({
url: `/webauthn/login`,
method: 'idaasPost',
data: data
});
\ No newline at end of file
import DialogPlus from './src/DialogPlus.vue'
export { DialogPlus }
<script setup lang="ts">
import { ElDialog, ElScrollbar } from 'element-plus'
import { propTypes } from '@/utils/propTypes'
import { computed, useAttrs, ref, unref, useSlots, watch, nextTick } from 'vue'
const slots = useSlots()
const props = defineProps({
modelValue: propTypes.bool.def(false),
scrollbar: propTypes.bool.def(true),
title: propTypes.string.def('Dialog'),
fullscreen: propTypes.bool.def(true),
maxHeight: propTypes.oneOfType([String, Number]).def('400px'),
minHeight: propTypes.oneOfType([String, Number]).def('140px')
})
const getBindValue = computed(() => {
const delArr: string[] = ['fullscreen', 'title', 'maxHeight']
const attrs = useAttrs()
const obj = { ...attrs, ...props }
for (const key in obj) {
if (delArr.indexOf(key) !== -1) {
delete obj[key]
}
}
return obj
})
const isFullscreen = ref(false)
const isNumber = (val) => {
return val && typeof props.maxHeight == 'number'
}
const dialogHeight = ref(isNumber(props.maxHeight) ? `${props.maxHeight}px` : props.maxHeight)
watch(
() => isFullscreen.value,
async (val: boolean) => {
await nextTick()
if (val) {
const windowHeight = document.documentElement.offsetHeight
dialogHeight.value = `${windowHeight - 55 - 60 - (slots.footer ? 63 : 0)}px`
} else {
dialogHeight.value = isNumber(props.maxHeight) ? `${props.maxHeight}px` : props.maxHeight
}
},
{
immediate: true
}
)
const dialogStyle = computed(() => {
return {
height: unref(dialogHeight),
'max-height': isNumber(props.maxHeight) ? `${props.maxHeight}px` : props.maxHeight,
'min-height': isNumber(props.minHeight) ? `${props.minHeight}px` : props.minHeight,
}
})
</script>
<template>
<ElDialog v-bind="getBindValue" :fullscreen="isFullscreen" destroy-on-close lock-scroll draggable top="15vh"
:close-on-click-modal="false" :show-close="true">
<template #header="{ close }">
<div class="flex justify-between items-center h-54px relative">
<slot name="title">
{{ title }}
</slot>
<!-- <div class="h-54px flex justify-between items-center absolute top-[50%] right-15px translate-y-[-50%]">
<Icon v-if="fullscreen" class="cursor-pointer is-hover !h-54px mr-10px"
:icon="isFullscreen ? 'radix-icons:exit-full-screen' : 'radix-icons:enter-full-screen'"
color="var(--el-color-info)" hover-color="var(--el-color-primary)" @click="toggleFull" />
<Icon class="cursor-pointer is-hover !h-54px" icon="ep:close" hover-color="var(--el-color-primary)"
color="var(--el-color-info)" @click="close" />
</div> -->
</div>
</template>
<ElScrollbar v-if="scrollbar" :style="dialogStyle">
<slot></slot>
</ElScrollbar>
<div v-else :style="dialogStyle">
<slot></slot>
</div>
<template v-if="slots.footer" #footer>
<slot name="footer"></slot>
</template>
</ElDialog>
</template>
<style lang="scss" scoped>
.el-overlay-dialog {
display: flex;
justify-content: center;
align-items: center;
:deep .el-dialog {
margin: 0 !important;
:deep .el-dialog__header {
padding: 0 20px;
height: 50px;
border-bottom: 1px solid #d9d9d9;
margin-right: 0;
display: block !important;
align-items: center;
.el-dialog__headerbtn {
width: 50px;
height: 50px;
top: 0;
}
}
&__header {
height: 54px;
padding: 0;
margin-right: 0 !important;
border-bottom: 1px solid var(--el-border-color);
}
&__body {
padding: 15px !important;
}
&__footer {
border-top: 1px solid var(--el-border-color);
}
&__headerbtn {
top: 0;
}
}
}
</style>
<script setup lang="ts">
import { FormPlus, FormSchema } from '@/components/FormPlus'
import { useForm } from '@/hooks/useForm'
import { propTypes } from "@/utils/propTypes";
import { reactive, unref, ref, computed } from 'vue'
import { ElButton, ElInput, FormItemProp } from 'element-plus'
import { useValidator } from '@/hooks/useValidator'
import { ComponentRef } from '@/types/global';
const props = defineProps({
schemaParam: {
type: Object as PropType<FormSchema[]>,
default: () => {
return []
}
},
labelPosition: propTypes.string.def('top'),
disabled: propTypes.bool.def(false),
model: {
type: Object as PropType<any>,
default: () => {
return {}
}
}
})
const formModel = computed(() => {
console.log(unref(props.model), 'model');
return props.model
})
const schemaData = computed(() => reactive<FormSchema[]>(unref(props.schemaParam)))
const { scrollToError } = useValidator()
const { formRegister, formMethods } = useForm()
const {
getFormResult,
setProps,
setValues,
setSchema,
getComponentExpose,
getFormItemExpose,
getElFormExpose,
getFormData
} = formMethods
/**
* 表单根据字段校验
* @param fields 待验证的字段
*/
const formValidation = async (fields: string[] = []) => { // 表单验证
const elFormExpose = await getElFormExpose()
return new Promise(async (resolve) => {
elFormExpose?.validateField(fields, (isValid) => {
if (isValid) {
resolve(isValid)
} else {
resolve(isValid)
}
})
})
}
const clearValidate = async (fields: string[] = []) => { // 表单验证
const elFormExpose = await getElFormExpose()
elFormExpose?.clearValidate(fields)
}
const inoutFocus = async () => {
const inputEl: ComponentRef<typeof ElInput> = await getComponentExpose('field1')
inputEl?.focus()
}
const inoutValidation = async () => {
const formItem = await getFormItemExpose('field1')
const inputEl: ComponentRef<typeof ElInput> = await getComponentExpose('field1')
inputEl?.focus()
formItem?.validate('focus', (val: boolean) => {
console.log(val)
})
}
const formValidate = (prop: FormItemProp, isValid: boolean, message: string) => {
// console.log(prop, isValid, message)
}
/**
* @param filterEmptyVal 是否过滤空值
*/
const getData = (filterEmptyVal = true) => {
if (filterEmptyVal) {
return new Promise(async (resolve) => {
const resultForm = await getFormData(filterEmptyVal)
resolve(resultForm)
})
} else {
return new Promise(async (resolve) => {
const resultForm = await getFormData(filterEmptyVal)
let res = getFormResult(resultForm, schemaData.value)
resolve(res)
})
}
}
const setValue = async (param = {}, reset = false) => {
const elFormExpose = await getElFormExpose()
if (reset) {
elFormExpose?.resetFields()
} else {
setValues(param)
}
}
const submitForm = () => {
return new Promise(async (resolve) => {
const elFormExpose = await getElFormExpose()
scrollToError()
elFormExpose?.validate((isValid) => {
if (isValid) {
resolve(isValid)
} else {
resolve(isValid)
}
})
})
}
// 子组建暴露的方法
defineExpose({
formValidation,
getFormResult,
clearValidate,
setSchema,
submitForm,
getData,
setValue: setValue,
setValues: setValue,
formMethods
})
</script>
<template>
<div>
<FormPlus :schema="schemaData" :disabled="disabled" :model="formModel" :isCol="true" :labelPosition="labelPosition"
@register="formRegister" @validate="formValidate" />
</div>
</template>
<style lang="scss" scoped>
.el-button {
margin-top: 10px;
}
</style>
import FormItem from './FormItem.vue';
export default FormItem
import FormPlus from './src/FormPlus.vue'
import type { FormSchema, FormSetProps } from './src/types'
export type {
ComponentNameEnum,
ComponentName,
InputComponentProps,
AutocompleteComponentProps,
InputNumberComponentProps,
SelectOption,
SelectComponentProps,
SelectV2ComponentProps,
CascaderComponentProps,
SwitchComponentProps,
RateComponentProps,
ColorPickerComponentProps,
TransferComponentProps,
RadioOption,
RadioGroupComponentProps,
RadioButtonComponentProps,
CheckboxOption,
CheckboxGroupComponentProps,
DividerComponentProps,
DatePickerComponentProps,
DateTimePickerComponentProps,
TimePickerComponentProps,
TimeSelectComponentProps,
ColProps,
FormSetProps,
FormItemProps,
FormSchema,
FormProps,
PlaceholderModel,
InputPasswordComponentProps,
TreeSelectComponentProps
} from './src/types'
export interface FormExpose {
setValues: (data: any) => void
setProps: (props: any) => void
delSchema: (field: string,deleteField:boolean) => void
addSchema: (formSchema: FormSchema, index?: number) => void
setSchema: (schemaProps: FormSetProps[]) => void
formModel: any
getComponentExpose: (field: string) => any
getFormItemExpose: (field: string) => any
}
export { FormPlus }
<script lang="tsx">
import { PropType, defineComponent, ref, computed, unref, watch, onMounted } from 'vue'
import {
ElForm,
ElFormItem,
ElRow,
ElCol,
FormRules,
ComponentSize
// FormItemProp
} from 'element-plus'
import { componentMap } from './helper/componentMap'
import { propTypes } from '@/utils/propTypes'
import {
setTextPlaceholder,
setGridProp,
setComponentProps,
setItemComponentSlots,
initModel
} from './helper'
import { useRenderSelect } from './components/useRenderSelect'
import { useRenderRadio } from './components/useRenderRadio'
import { useRenderCheckbox } from './components/useRenderCheckbox'
import { get, set } from 'lodash-es'
import { FormProps } from './types'
import {
FormSchema,
FormSetProps,
ComponentNameEnum,
SelectComponentProps,
RadioGroupComponentProps,
CheckboxGroupComponentProps
} from './types'
import { ComponentRef, Recordable } from '@/types/global'
import { Slots } from 'vue'
const { renderSelectOptions } = useRenderSelect()
const { renderRadioOptions } = useRenderRadio()
const { renderCheckboxOptions } = useRenderCheckbox()
export const getSlot = (slots: Slots, slot = 'default', data?: Recordable) => {
// Reflect.has 判断一个对象是否存在某个属性
if (!slots || !Reflect.has(slots, slot)) {
return null
}
if (typeof slots[slot] !== 'function') {
console.error(`${slot} is not a function!`)
return null
}
const slotFn = slots[slot]
if (!slotFn) return null
return slotFn(data)
}
export default defineComponent({
name: 'Form',
props: {
// 生成Form的布局结构数组
schema: {
type: Array as PropType<FormSchema[]>,
default: () => []
},
// 是否需要栅格布局
isCol: propTypes.bool.def(true),
// 表单数据对象
model: {
type: Object as PropType<any>,
default: () => ({})
},
// 是否自动设置placeholder
autoSetPlaceholder: propTypes.bool.def(true),
// 是否自定义内容
isCustom: propTypes.bool.def(false),
// 表单label宽度
labelWidth: propTypes.oneOfType([String, Number]).def('auto'),
rules: {
type: Object as PropType<FormRules>,
default: () => ({})
},
labelPosition: propTypes.oneOf(['left', 'right', 'top']).def('right'),
labelSuffix: propTypes.string.def(''),
hideRequiredAsterisk: propTypes.bool.def(false),
requireAsteriskPosition: propTypes.oneOf(['left', 'right']).def('right'),
showMessage: propTypes.bool.def(true),
inlineMessage: propTypes.bool.def(false),
statusIcon: propTypes.bool.def(false),
validateOnRuleChange: propTypes.bool.def(true),
size: {
type: String as PropType<ComponentSize>,
default: undefined
},
disabled: propTypes.bool.def(false),
scrollToError: propTypes.bool.def(false),
scrollToErrorOffset: propTypes.oneOfType([Boolean, Object]).def(undefined)
// onValidate: {
// type: Function as PropType<(prop: FormItemProp, isValid: boolean, message: string) => void>,
// default: () => {}
// }
},
emits: ['register'],
setup(props, { slots, expose, emit }) {
// element form 实例
const elFormRef = ref<ComponentRef<typeof ElForm>>()
const mergeProps = ref<FormProps>({})
const getProps = computed(() => {
const propsObj = { ...props }
Object.assign(propsObj, unref(mergeProps))
return propsObj
})
// 存储表单实例
const formComponents = ref({})
// 存储form-item实例
const formItemComponents = ref({})
// 表单数据
const formModel = ref<any>(props.model)
onMounted(() => {
emit('register', unref(elFormRef)?.$parent, unref(elFormRef))
})
// 对表单赋值
const setValues = (data = {}) => {
formModel.value = Object.assign(unref(formModel), data)
}
const setProps = (props: FormProps = {}) => {
mergeProps.value = Object.assign(unref(mergeProps), props)
}
const delSchema = (field: string,deleteField:boolean) => {
const { schema } = unref(getProps)
const index = schema.findIndex(v => v.field === field);
if (index > -1) {
// #删除对应的字段
if (formModel.value[field] && deleteField) delete formModel.value[field]
schema.splice(index, 1)
}
}
const addSchema = (formSchema: FormSchema, index?: number) => {
const { schema } = unref(getProps)
if (index !== void 0) {
schema.splice(index, 0, formSchema)
return
}
schema.push(formSchema)
}
const addSchemas = (formSchemas: FormSchema[], index?: number) => {
const { schema } = unref(getProps)
if (index !== void 0) {
schema.splice(index, 0, ...formSchemas)
} else {
schema.push(...formSchemas)
}
}
const setSchema = (schemaProps: FormSetProps[]) => {
const { schema } = unref(getProps)
for (const v of schema) {
for (const item of schemaProps) {
if (v.field === item.field) {
set(v, item.path, item.value)
}
}
}
}
const getOptions = async (fn: Function, item: FormSchema) => {
const options = await fn()
setSchema([
{
field: item.field,
path:
item.component === ComponentNameEnum.TREE_SELECT
? 'componentProps.data'
: 'componentProps.options',
value: options
}
])
}
/**
* @description: 获取表单组件实例
* @param filed 表单字段
*/
const getComponentExpose = (filed: string) => {
return unref(formComponents)[filed]
}
/**
* @description: 获取formItem实例
* @param filed 表单字段
*/
const getFormItemExpose = (filed: string) => {
return unref(formItemComponents)[filed]
}
const setComponentRefMap = (ref: any, filed: string) => {
formComponents.value[filed] = ref
}
const setFormItemRefMap = (ref: any, filed: string) => {
formItemComponents.value[filed] = ref
}
expose({
setValues,
formModel,
setProps,
delSchema,
addSchema,
addSchemas,
setSchema,
getComponentExpose,
getFormItemExpose
})
// 监听表单结构化数组,重新生成formModel
watch(
() => unref(getProps).schema,
(schema = []) => {
formModel.value = initModel(schema, unref(formModel))
},
{
immediate: true,
deep: true
}
)
// 渲染包裹标签,是否使用栅格布局
const renderWrap = () => {
const { isCol } = unref(getProps)
const content = isCol ? (
<ElRow gutter={20}>{renderFormItemWrap()}</ElRow>
) : (
renderFormItemWrap()
)
return content
}
// 是否要渲染el-col
const renderFormItemWrap = () => {
// hidden属性表示隐藏,不做渲染
const { schema = [], isCol } = unref(getProps)
// schema.forEach(item=>{ // 默认显示
// item.visible = true
// })
return schema
.filter((v) => !v.remove && (v.visible ?? true))
.map((item) => {
// 如果是 Divider 组件,需要自己占用一行
const isDivider = item.component === 'Divider'
const Com = componentMap['Divider'] as ReturnType<typeof defineComponent>
return isDivider ? (
<Com {...{ contentPosition: 'left', ...item.componentProps }}>{item?.label}</Com>
) : isCol ? (
// 如果需要栅格,需要包裹 ElCol
<ElCol {...setGridProp(item.colProps)}>{renderFormItem(item)}</ElCol>
) : (
renderFormItem(item)
)
})
}
// 渲染formItem
const renderFormItem = (item: FormSchema) => {
// 如果有optionApi,优先使用optionApi
if (item.optionApi) {
// 内部自动调用接口,不影响其它渲染
getOptions(item.optionApi, item)
}
const formItemSlots: Recordable = {
default: () => {
if (item?.formItemProps?.slots?.default) {
return item?.formItemProps?.slots?.default(formModel.value)
} else {
const Com = componentMap[item.component as string] as ReturnType<typeof defineComponent>
const { autoSetPlaceholder } = unref(getProps)
const componentSlots = (item?.componentProps as any)?.slots || {}
const slotsMap: Recordable = {
...setItemComponentSlots(componentSlots)
}
// 如果是select组件,并且没有自定义模板,自动渲染options
if (item.component === ComponentNameEnum.SELECT) {
slotsMap.default = !componentSlots.default
? () => renderSelectOptions(item)
: () => {
return componentSlots.default(
unref((item?.componentProps as SelectComponentProps)?.options)
)
}
}
// 虚拟列表
if (item.component === ComponentNameEnum.SELECT_V2 && componentSlots.default) {
slotsMap.default = ({ item }) => {
return componentSlots.default(item)
}
}
// 单选框组和按钮样式
if (
item.component === ComponentNameEnum.RADIO_GROUP ||
item.component === ComponentNameEnum.RADIO_BUTTON
) {
slotsMap.default = !componentSlots.default
? () => renderRadioOptions(item,formModel)
: () => {
return componentSlots.default(
unref((item?.componentProps as CheckboxGroupComponentProps)?.options)
)
}
}
// 多选框组和按钮样式
if (
item.component === ComponentNameEnum.CHECKBOX_GROUP ||
item.component === ComponentNameEnum.CHECKBOX_BUTTON
) {
slotsMap.default = !componentSlots.default
? () => renderCheckboxOptions(item)
: () => {
return componentSlots.default(
unref((item?.componentProps as RadioGroupComponentProps)?.options)
)
}
}
const Comp = () => {
// 如果field是多层路径,需要转换成对象
const itemVal = computed({
get: () => {
return get(formModel.value, item.field)
},
set: (val) => {
set(formModel.value, item.field, val)
}
})
return (
<Com
vModel={itemVal.value}
// 数字框禁用e常数
onkeypress={(event)=>{
if (item.component === ComponentNameEnum.INPUT_NUMBER) {
return (/[\d . -]/.test(String.fromCharCode(event.keyCode)))
}
}}
ref={(el: any) => setComponentRefMap(el, item.field)}
{...(autoSetPlaceholder && setTextPlaceholder(item))}
{...setComponentProps(item,unref(getProps))}
style={
item.componentProps?.style || {
width: '100%'
}
}
>
{{ ...slotsMap }}
</Com>
)
}
return <>{Comp()}</>
}
}
}
if (item?.formItemProps?.slots?.label) {
formItemSlots.label = (...args: any[]) => {
return (item?.formItemProps?.slots as any)?.label(...args)
}
}
if (item?.formItemProps?.slots?.error) {
formItemSlots.error = (...args: any[]) => {
return (item?.formItemProps?.slots as any)?.error(...args)
}
}
return (
<ElFormItem
v-show={!item.hidden}
ref={(el: any) => setFormItemRefMap(el, item.field)}
{...(item.formItemProps || {})}
prop={item.field}
label={item.label || ''}
>
{formItemSlots}
</ElFormItem>
)
}
// 过滤传入Form组件的属性
const getFormBindValue = () => {
// 避免在标签上出现多余的属性
const delKeys = ['schema', 'isCol', 'autoSetPlaceholder', 'isCustom', 'model']
const props = { ...unref(getProps) }
for (const key in props) {
if (delKeys.indexOf(key) !== -1) {
delete props[key]
}
}
return props as FormProps
}
return () => (
<ElForm
ref={elFormRef}
{...getFormBindValue()}
model={unref(getProps).isCustom ? unref(getProps).model : formModel}
>
{{
// 如果需要自定义,就什么都不渲染,而是提供默认插槽
default: () => {
const { isCustom } = unref(getProps)
return isCustom ? getSlot(slots, 'default') : renderWrap()
}
}}
</ElForm>
)
}
})
</script>
<style lang="scss" scoped>
// .@{elNamespace}-form.@{namespace}-form .@{elNamespace}-row {
// margin-right: 0 !important;
// margin-left: 0 !important;
// }
// .@{elNamespace}-form--inline .@{elNamespace}-input {
// width: 245px;
// }
:deep(.el-form-item) {
.el-form-item__label {
margin-bottom: 2px !important
}
.el-input-number .el-input__inner {
text-align: left;
}
.el-radio {
.el-radio__inner::after {
width: 10px;
height: 10px;
}
.el-radio__input.is-checked .el-radio__inner {
background-color: inherit;
&::after {
background-color: var(--el-color-primary);
}
}
}
}
</style>
interface RenderHeader {
label:string // 列label
labelClass?:string
style?:string
iconProps?:IconProps
popoverProps:PopoverProps
}
interface IconProps {
iconClass?:string // 图标类名
icon:string // 图标名称
}
interface PopoverProps {
visible?:boolean
popoverContent:string // 提示内容
popoverWidth?:number // 提示框宽度
popoverTrigger? : 'click' | 'focus' | 'hover' | 'contextmenu' // 触发方式
popoverTitle?:string // 提示标题
}
export const useRenderAmountInput = () => {
/**
* 金额input渲染函数
* @param param
* @param isDetail 是否详情
* @returns
*/
const renderAmountInput = (param:RenderHeader,isDetail?:boolean) => {
return (
<>
</>
);
}
return {
renderAmountInput
}
}
\ No newline at end of file
import { FormSchema, ComponentNameEnum, CheckboxGroupComponentProps } from '../types'
import { ElCheckbox, ElCheckboxButton } from 'element-plus'
import { defineComponent } from 'vue'
export const useRenderCheckbox = () => {
const renderCheckboxOptions = (item: FormSchema) => {
// 如果有别名,就取别名
const componentProps = item?.componentProps as CheckboxGroupComponentProps
const valueAlias = componentProps?.props?.value || 'value'
const labelAlias = componentProps?.props?.label || 'label'
const disabledAlias = componentProps?.props?.disabled || 'disabled'
const Com = (
item.component === ComponentNameEnum.CHECKBOX_GROUP ? ElCheckbox : ElCheckboxButton
) as ReturnType<typeof defineComponent>
return componentProps?.options?.map((option) => {
const { value, ...other } = option
return (
<Com
{...other}
disabled={option[disabledAlias || 'disabled']}
label={option[valueAlias || 'value']}
>
{option[labelAlias || 'label']}
</Com>
)
})
}
return {
renderCheckboxOptions
}
}
import { FormSchema, ComponentNameEnum, RadioGroupComponentProps } from '../types'
import { ElRadio, ElRadioButton, ElIcon } from 'element-plus'
import { defineComponent } from 'vue'
import { Check } from "@element-plus/icons-vue"
import '../styles/radio.scss'
export const useRenderRadio = () => {
const renderRadioOptions = (item: FormSchema, formModel: any) => {
// 如果有别名,就取别名
const { field } = item
const componentProps = item?.componentProps as RadioGroupComponentProps
const valueAlias = componentProps?.props?.value || 'value'
const labelAlias = componentProps?.props?.label || 'label'
const disabledAlias = componentProps?.props?.disabled || 'disabled'
const Com = (
item.component === ComponentNameEnum.RADIO_GROUP ? ElRadio : ElRadioButton
) as ReturnType<typeof defineComponent>
return componentProps?.options?.map((option) => {
const { value, ...other } = option
return (
<div class={other.border ? 'radio_panel' : ''}>
<Com
{...other}
disabled={option[disabledAlias || 'disabled']}
label={option[valueAlias || 'value']}
style='margin-right:8px'
>
{option[labelAlias || 'label']}
{
other.border ?
(<ElIcon class={ formModel.value[field] == value ? 'active corner_mark':'corner_mark' }>
<Check></Check>
</ElIcon>) : undefined
}
</Com>
</div>
)
})
}
return {
renderRadioOptions
}
}
import { ElOption, ElOptionGroup } from 'element-plus'
import { FormSchema, SelectComponentProps, SelectOption } from '../types'
export const useRenderSelect = () => {
// 渲染 select options
const renderSelectOptions = (item: FormSchema) => {
const componentsProps = item?.componentProps as SelectComponentProps
const optionGroupDefaultSlot = componentsProps?.slots?.optionGroupDefault
// 如果有别名,就取别名
const labelAlias = componentsProps?.props?.label
const keyAlias = componentsProps?.props?.key
return componentsProps?.options?.map((option) => {
if (option?.options?.length) {
return optionGroupDefaultSlot ? (
optionGroupDefaultSlot(option)
) : (
<ElOptionGroup label={option[labelAlias || 'label']} key={option[keyAlias || 'key']}>
{{
default: () =>
option?.options?.map((v) => {
return renderSelectOptionItem(item, v)
})
}}
</ElOptionGroup>
)
} else {
return renderSelectOptionItem(item, option)
}
})
}
// 渲染 select option item
const renderSelectOptionItem = (item: FormSchema, option: SelectOption) => {
// 如果有别名,就取别名
const componentsProps = item.componentProps as SelectComponentProps
const labelAlias = componentsProps?.props?.label
const valueAlias = componentsProps?.props?.value
const keyAlias = componentsProps?.props?.key
const optionDefaultSlot = componentsProps.slots?.optionDefault
return (
<ElOption
{...option}
key={option[keyAlias || 'key']}
label={option[labelAlias || 'label']}
value={option[valueAlias || 'value']}
disabled={option.disabled}
>
{{
default: () => (optionDefaultSlot ? optionDefaultSlot(option) : undefined)
}}
</ElOption>
)
}
return {
renderSelectOptions
}
}
import type { Component } from 'vue'
import {
ElCascader,
ElCheckboxGroup,
ElColorPicker,
ElDatePicker,
ElInput,
ElInputNumber,
ElRadioGroup,
ElRate,
ElSelect,
ElSelectV2,
ElSlider,
ElSwitch,
ElTimePicker,
ElTimeSelect,
ElTransfer,
ElAutocomplete,
ElDivider,
ElTreeSelect,
ElUpload
} from 'element-plus'
// import { InputPassword } from '@/components/InputPassword'
// import { Editor } from '@/components/Editor'
// import { JsonEditor } from '@/components/JsonEditor'
// import { IconPicker } from '@/components/IconPicker'
import { ComponentName } from '../types'
import { Recordable } from '@/types/global'
const componentMap: Recordable<Component, ComponentName> = {
RadioGroup: ElRadioGroup,
RadioButton: ElRadioGroup,
CheckboxGroup: ElCheckboxGroup,
CheckboxButton: ElCheckboxGroup,
Input: ElInput,
Autocomplete: ElAutocomplete,
InputNumber: ElInputNumber,
Select: ElSelect,
Cascader: ElCascader,
Switch: ElSwitch,
Slider: ElSlider,
TimePicker: ElTimePicker,
DatePicker: ElDatePicker,
Rate: ElRate,
ColorPicker: ElColorPicker,
Transfer: ElTransfer,
Divider: ElDivider,
TimeSelect: ElTimeSelect,
SelectV2: ElSelectV2,
TreeSelect: ElTreeSelect,
Upload: ElUpload,
// InputPassword: InputPassword,
// Editor: Editor,
// JsonEditor: JsonEditor,
// IconPicker: IconPicker
}
export { componentMap }
import { firstUpperCase, humpToDash } from '@/utils/common'
import { PlaceholderModel, FormSchema, ComponentNameEnum, ColProps } from '../types'
import { set, get } from 'lodash-es'
import { Recordable } from '@/types/global'
/**
*
* @param schema 对应组件数据
* @returns 返回提示信息对象
* @description 用于自动设置placeholder
*/
export const setTextPlaceholder = (schema: FormSchema): PlaceholderModel => {
const textMap = [
ComponentNameEnum.INPUT,
ComponentNameEnum.AUTOCOMPLETE,
ComponentNameEnum.INPUT_NUMBER,
ComponentNameEnum.INPUT_PASSWORD
]
const selectMap = [
ComponentNameEnum.SELECT,
ComponentNameEnum.TIME_PICKER,
ComponentNameEnum.DATE_PICKER,
ComponentNameEnum.TIME_SELECT,
ComponentNameEnum.SELECT_V2
]
if (textMap.includes(schema?.component as ComponentNameEnum)) {
return {
placeholder: '请输入'
}
}
if (selectMap.includes(schema?.component as ComponentNameEnum)) {
// 一些范围选择器
const twoTextMap = ['datetimerange', 'daterange', 'monthrange', 'datetimerange', 'daterange']
if (
twoTextMap.includes(
((schema?.componentProps as any)?.type ||
(schema?.componentProps as any)?.isRange) as string
)
) {
return {
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
rangeSeparator: '-'
}
} else {
return {
placeholder: '请选择'
}
}
}
return {}
}
/**
*
* @param col 内置栅格
* @returns 返回栅格属性
* @description 合并传入进来的栅格属性
*/
export const setGridProp = (col: ColProps = {}): ColProps => {
const colProps: ColProps = {
// 如果有span,代表用户优先级更高,所以不需要默认栅格
...(col.span
? {}
: {
xs: 24,
sm: 12,
md: 12,
lg: 12,
xl: 12
}),
...col
}
return colProps
}
/**
*
* @param item 传入的组件属性
* @param props 传入表单所有属性
* @returns 默认添加 clearable 属性
*/
export const setComponentProps = (item: FormSchema,props:any): Recordable => {
// const notNeedClearable = ['ColorPicker']
// 拆分事件并组合
const onEvents = (item?.componentProps as any)?.on || {}
const newOnEvents: Recordable = {}
for (const key in onEvents) {
if (onEvents[key]) {
newOnEvents[`on${firstUpperCase(key)}`] = (...args: any[]) => {
onEvents[key](...args)
}
}
}
const componentProps: Recordable = {
clearable: true, // 默认全局开启清空属性
...item.componentProps,
...newOnEvents
}
// 需要删除额外的属性
if (componentProps.slots) {
delete componentProps.slots
}
if (componentProps.on) {
delete componentProps.on
}
if ((item.component === 'Select' || item.component === 'Cascader' || item.component === 'TreeSelect') && (componentProps.filterable === undefined)) {
componentProps.filterable = true
}
if ((item.component === 'Select' || item.component === 'TreeSelect') && (componentProps.fitInputWidth === undefined)) {
componentProps.fitInputWidth = true
}
if ((item.component === 'InputNumber') && (componentProps.controls === undefined)) { // 默认全局取消加减号按钮
componentProps.controls = false
}
if ((item.component === 'InputNumber') && componentProps.precision === undefined) { // 默认全局2位小数
componentProps.precision = (props.disabled || componentProps.disabled) ? 3 : 2
}
if ((item.component === 'InputNumber') && componentProps.min === undefined) { // 默认默认最小值0
componentProps.min = 0
}
// if ((item.component === 'Input') && componentProps.formatter === undefined) { // 默认添加过滤前后空格
// componentProps.formatter = (value:string) => value.trim()
// }
return componentProps
}
/**
*
* @param formModel 表单数据
* @param slotsProps 插槽属性
*/
export const setItemComponentSlots = (slotsProps: Recordable = {}): Recordable => {
const slotObj: Recordable = {}
for (const key in slotsProps) {
if (slotsProps[key]) {
if (typeof slotsProps[key] == 'function') {
slotObj[humpToDash(key)] = (...args: any[]) => {
return slotsProps[key]?.(...args)
}
} else {
slotObj[humpToDash(key)] = () => {
return slotsProps[key]
}
}
}
}
return slotObj
}
/**
*
* @param schema Form表单结构化数组
* @param formModel FormMoel
* @returns FormMoel
* @description 生成对应的formModel
*/
export const initModel = (schema: FormSchema[], formModel: Recordable) => {
const model: Recordable = { ...formModel }
schema.map((v) => {
if (v.remove) {
delete model[v.field]
} else if (v.component !== 'Divider') {
// const hasField = Reflect.has(model, v.field)
const hasField = get(model, v.field)
// 如果先前已经有值存在,则不进行重新赋值,而是采用现有的值
set(
model,
v.field,
hasField !== void 0 ? get(model, v.field) : v.value !== void 0 ? v.value : undefined
)
// model[v.field] = hasField ? model[v.field] : v.value !== void 0 ? v.value : undefined
}
})
return model
}
.radio_panel {
.el-radio {
&.is-bordered {
padding: 0;
position: relative;
border-radius:2px;
.el-radio__input {
position: absolute;
opacity: 0;
}
.el-radio__label {
padding: 0 15px;
font-size: 14px;
position: relative;
.corner_mark {
position: absolute;
right: 0;
bottom: -4px;
width: 18px;
height: 18px;
color: #fff;
svg {
width: 9px;
height: 9px;
position: absolute;
right: 0;
bottom: 2px;
z-index: 1;
}
&.active {
&::after {
content: "";
position: absolute;
border: 9px solid var(--el-color-primary);
border-top-color: transparent;
border-left-color: transparent;
}
}
}
}
.el-radio__input.is-checked+.el-radio__label {
color: var(--el-color-primary);
}
}
}
}
\ No newline at end of file
import {
AutocompleteProps,
InputNumberProps,
CascaderProps,
CascaderNode,
CascaderValue,
SwitchProps,
ComponentSize,
InputProps,
RateProps,
ColorPickerProps,
TransferProps,
RadioGroupProps,
RadioButtonProps,
CheckboxGroupProps,
DividerProps,
DatePickerProps,
FormItemProps as ElFormItemProps,
FormProps as ElFormProps,
ISelectProps,
UploadProps
} from 'element-plus'
import { IEditorConfig } from '@wangeditor/editor'
import { JsonEditorProps } from '@/components/JsonEditor'
import { CSSProperties } from 'vue'
export interface PlaceholderModel {
placeholder?: string
startPlaceholder?: string
endPlaceholder?: string
rangeSeparator?: string
}
export enum ComponentNameEnum {
RADIO_GROUP = 'RadioGroup',
RADIO_BUTTON = 'RadioButton',
CHECKBOX_GROUP = 'CheckboxGroup',
CHECKBOX_BUTTON = 'CheckboxButton',
INPUT = 'Input',
AUTOCOMPLETE = 'Autocomplete',
INPUT_NUMBER = 'InputNumber',
SELECT = 'Select',
CASCADER = 'Cascader',
SWITCH = 'Switch',
SLIDER = 'Slider',
TIME_PICKER = 'TimePicker',
DATE_PICKER = 'DatePicker',
RATE = 'Rate',
COLOR_PICKER = 'ColorPicker',
TRANSFER = 'Transfer',
DIVIDER = 'Divider',
TIME_SELECT = 'TimeSelect',
SELECT_V2 = 'SelectV2',
INPUT_PASSWORD = 'InputPassword',
EDITOR = 'Editor',
TREE_SELECT = 'TreeSelect',
UPLOAD = 'Upload',
JSON_EDITOR = 'JsonEditor',
ICON_PICKER = 'IconPicker'
}
type CamelCaseComponentName = keyof typeof ComponentNameEnum extends infer K
? K extends string
? K extends `${infer A}_${infer B}`
? `${Capitalize<Lowercase<A>>}${Capitalize<Lowercase<B>>}`
: Capitalize<Lowercase<K>>
: never
: never
export type ComponentName = CamelCaseComponentName
export interface InputPasswordComponentProps {
strength?: boolean
style?: CSSProperties
}
export interface InputComponentProps extends Partial<InputProps> {
rows?: number
on?: {
blur?: (event: FocusEvent) => void
focus?: (event: FocusEvent) => void
change?: (value: string | number) => void
clear?: () => void
input?: (value: string | number) => void
}
slots?: {
prefix?: (...args: any[]) => JSX.Element | null
suffix?: (...args: any[]) => JSX.Element | null
prepend?: (...args: any[]) => JSX.Element | null
append?: (...args: any[]) => JSX.Element | null
}
style?: CSSProperties
}
export interface AutocompleteComponentProps extends Partial<AutocompleteProps> {
on?: {
select?: (item: any) => void
change?: (value: string | number) => void
}
slots?: {
default?: (...args: any[]) => JSX.Element | null
prefix?: (...args: any[]) => JSX.Element | null
suffix?: (...args: any[]) => JSX.Element | null
prepend?: (...args: any[]) => JSX.Element | null
append?: (...args: any[]) => JSX.Element | null
}
style?: CSSProperties
}
export interface InputNumberComponentProps extends Partial<InputNumberProps> {
on?: {
change?: (currentValue: number | undefined, oldValue: number | undefined) => void
blur?: (event: FocusEvent) => void
focus?: (event: FocusEvent) => void
}
style?: CSSProperties
}
export interface SelectOption {
label?: string
disabled?: boolean
value?: any
key?: string | number
options?: SelectOption[]
[key: string]: any
}
export interface SelectComponentProps extends Omit<Partial<ISelectProps>, 'options'> {
/**
* 数据源的字段别名
*/
props?: {
key?: string
value?: string
label?: string
children?: string
}
on?: {
change?: (value: string | number | boolean | Object) => void
visibleChange?: (visible: boolean) => void
removeTag?: (tag: any) => void
clear?: () => void
blur?: (event: FocusEvent) => void
focus?: (event: FocusEvent) => void
}
slots?: {
default?: (options: SelectOption[]) => JSX.Element[] | null
optionGroupDefault?: (item: SelectOption) => JSX.Element
optionDefault?: (option: SelectOption) => JSX.Element | null
prefix?: (...args: any[]) => JSX.Element | null
empty?: (...args: any[]) => JSX.Element | null
}
options?: SelectOption[]
style?: CSSProperties
}
export interface SelectV2ComponentProps {
multiple?: boolean
disabled?: boolean
valueKey?: string
size?: ComponentSize
clearable?: boolean
clearIcon?: string | JSX.Element | null
collapseTags?: boolean
multipleLimit?: number
name?: string
effect?: string
autocomplete?: string
placeholder?: string
filterable?: boolean
allowCreate?: boolean
reserveKeyword?: boolean
noDataText?: string
popperClass?: string
teleported?: boolean
persistent?: boolean
popperOptions?: any
automaticDropdown?: boolean
height?: number
scrollbarAlwaysOn?: boolean
remote?: boolean
remoteMethod?: (query: string) => void
validateEvent?: boolean
placement?: AutocompleteProps['placement']
collapseTagsTooltip?: boolean
on?: {
change?: (value: string | number | boolean | Object) => void
visibleChange?: (visible: boolean) => void
removeTag?: (tag: any) => void
clear?: () => void
blur?: (event: FocusEvent) => void
focus?: (event: FocusEvent) => void
}
options?: SelectOption[]
slots?: {
default?: (option: SelectOption) => JSX.Element | null
}
style?: CSSProperties
}
export interface CascaderComponentProps {
options?: Record<string, unknown>[]
props?: CascaderProps
size?: ComponentSize
placeholder?: string
disabled?: boolean
clearable?: boolean
showAllLevels?: boolean
collapseTags?: boolean
collapseTagsTooltip?: boolean
separator?: string
filterable?: boolean
filterMethod?: (node: CascaderNode, keyword: string) => boolean
debounce?: number
beforeFilter?: (value: string) => boolean
popperClass?: string
teleported?: boolean
tagType?: ElementPlusInfoType
validateEvent?: boolean
on?: {
change?: (value: CascaderValue) => void
expandChange?: (value: CascaderValue) => void
blur?: (event: FocusEvent) => void
focus?: (event: FocusEvent) => void
visibleChange?: (value: boolean) => void
removeTag?: (value: CascaderNode['valueByOption']) => void
}
slots?: {
default?: (...args: any[]) => JSX.Element | null
empty?: (...args: any[]) => JSX.Element | null
}
style?: CSSProperties
}
export interface SwitchComponentProps extends Partial<SwitchProps> {
on?: {
change?: (value: boolean | string | number) => void
}
style?: CSSProperties
}
export interface RateComponentProps extends Partial<RateProps> {
on?: {
change?: (value: number) => void
}
style?: CSSProperties
}
export interface ColorPickerComponentProps extends Partial<ColorPickerProps> {
on?: {
change?: (value: string) => void
activeChange?: (value: string) => void
}
style?: CSSProperties
}
export interface TransferComponentProps extends Partial<TransferProps> {
on?: {
change?: (
value: number | string,
direction: 'left' | 'right',
movedKeys: string[] | number[]
) => void
leftCheckChange?: (value: any[]) => void
rightCheckChange?: (value: any[]) => void
}
slots?: {
default?: (...args: any[]) => JSX.Element | null
leftFooter?: (...args: any[]) => JSX.Element | null
rightFooter?: (...args: any[]) => JSX.Element | null
}
style?: CSSProperties
}
export interface RadioOption {
label?: string
value?: string | number | boolean
disabled?: boolean
border?: boolean
size?: ComponentSize
name?: string
[key: string]: any
}
export interface RadioGroupComponentProps extends Partial<RadioGroupProps> {
options?: RadioOption[]
/**
* 数据源的字段别名
*/
props?: {
label?: string
value?: string
disabled?: string
}
on?: {
change?: (value: string | number | boolean) => void
}
slots?: {
default?: (...args: any[]) => JSX.Element[] | null
}
style?: CSSProperties
}
export interface RadioButtonComponentProps extends Partial<RadioButtonProps> {
options?: RadioOption[]
/**
* 数据源的字段别名
*/
props?: {
label?: string
value?: string
disabled?: string
}
on?: {
change?: (value: string | number | boolean) => void
}
slots?: {
default?: (...args: any[]) => JSX.Element[] | null
}
style?: CSSProperties
}
export interface CheckboxOption {
label?: string
value?: string | number | boolean
disabled?: boolean
trueLabel?: string | number
falseLabel?: string | number
border?: boolean
size?: ComponentSize
name?: string
checked?: boolean
indeterminate?: boolean
validateEvent?: boolean
tabindex?: number | string
id?: string
controls?: boolean
[key: string]: any
}
export interface CheckboxGroupComponentProps extends Partial<CheckboxGroupProps> {
options?: CheckboxOption[]
/**
* 数据源的字段别名
*/
props?: {
label?: string
value?: string
disabled?: string
}
on?: {
change?: (value: string | number | boolean) => void
}
slots?: {
default?: (...args: any[]) => JSX.Element[] | null
}
style?: CSSProperties
}
export interface DividerComponentProps extends Partial<DividerProps> {
on?: {
change?: (value: number) => void
input?: (value: number) => void
}
style?: CSSProperties
}
export interface DatePickerComponentProps extends Partial<DatePickerProps> {
on?: {
change?: (value: string | Date | number | string[]) => void
blur?: (event: FocusEvent) => void
focus?: (event: FocusEvent) => void
calendarChange?: (val: [Date, Date]) => void
panelChange?: (date, mode, view) => void
visibleChange?: (visibility: boolean) => void
}
slots?: {
default?: (...args: any[]) => JSX.Element | null
rangeSeparator?: (...args: any[]) => JSX.Element | null
}
style?: CSSProperties
}
export interface DateTimePickerComponentProps {
readonly?: boolean
disabled?: boolean
editable?: boolean
clearable?: boolean
size?: ComponentSize
placeholder?: string
startPlaceholder?: string
endPlaceholder?: string
timeArrowControl?: boolean
type?: 'year' | 'month' | 'date' | 'datetime' | 'datetimerange' | 'daterange' | 'week'
format?: string
popperClass?: string
rangeSeparator?: string
defaultValue?: Date | [Date, Date]
defaultTime?: Date | [Date, Date]
valueFormat?: string
id?: string
name?: string
unlinkPanels?: boolean
prefixIcon?: string | JSX.Element
clearIcon?: string | JSX.Element
shortcuts?: Array<{ text: string; value: Date | Function }>
disabledDate?: (date: Date) => boolean
cellClassName?: string | ((date: Date) => string | undefined)
teleported?: boolean
on?: {
change?: (value: string | Date | number | string[]) => void
blur?: (event: FocusEvent) => void
focus?: (event: FocusEvent) => void
calendarChange?: (val: [Date, Date]) => void
visibleChange?: (visibility: boolean) => void
}
slots?: {
default?: (...args: any[]) => JSX.Element | null
rangeSeparator?: (...args: any[]) => JSX.Element | null
}
style?: CSSProperties
}
export interface TimePickerComponentProps {
readonly?: boolean
disabled?: boolean
editable?: boolean
clearable?: boolean
size?: ComponentSize
placeholder?: string
startPlaceholder?: string
endPlaceholder?: string
isRange?: boolean
arrowControl?: boolean
popperClass?: string
rangeSeparator?: string
format?: string
defaultValue?: Date | [Date, Date]
id?: string
name?: string
label?: string
prefixIcon?: string | JSX.Element
clearIcon?: string | JSX.Element
disabledHours?: (role: string, comparingDate?: any) => number[]
disabledMinutes?: (hour: number, role: string, comparingDate?: any) => number[]
disabledSeconds?: (hour: number, minute: number, role: string, comparingDate?: any) => number[]
teleported?: boolean
tabindex?: number | string
on?: {
change: (
val: number | string | Date | [number, number] | [string, string] | [Date, Date]
) => void
blur?: (event: FocusEvent) => void
focus?: (event: FocusEvent) => void
visibleChange?: (visibility: boolean) => void
}
style?: CSSProperties
}
export interface TimeSelectComponentProps {
disabled?: boolean
editable?: boolean
clearable?: boolean
size?: ComponentSize
placeholder?: string
name?: string
effect?: string
prefixIcon?: string | JSX.Element
clearIcon?: string | JSX.Element
start?: string
end?: string
step?: string
minTime?: string
maxTime?: string
format?: string
on?: {
change?: (val: string) => void
blur?: (event: FocusEvent) => void
focus?: (event: FocusEvent) => void
}
style?: CSSProperties
}
export interface EditorComponentProps {
editorConfig?: IEditorConfig
style?: CSSProperties
}
export interface ColProps {
span?: number
xs?: number
sm?: number
md?: number
lg?: number
xl?: number
tag?: string
}
export interface FormSetProps {
field: string
path: string
value: any
}
export interface FormItemProps extends Partial<ElFormItemProps> {
style?: CSSProperties
slots?: {
default?: (...args: any[]) => JSX.Element | null
label?: (...args: any[]) => JSX.Element | null
error?: (...args: any[]) => JSX.Element | null
}
}
export interface UploadComponentProps extends Partial<UploadProps> {
slots?: {
default?: (...args: any[]) => JSX.Element | null
trigger?: (...args: any[]) => JSX.Element | null
tip?: (...args: any[]) => JSX.Element | null
file?: (...args: any[]) => JSX.Element | null
}
style?: CSSProperties
}
export interface TreeSelectComponentProps
extends Omit<Partial<SelectComponentProps>, 'props' | 'on' | 'slots'> {
data?: any[]
emptyText?: string
nodeKey?: string
props?: {
children?: string
label?: string | ((...args: any[]) => string)
disabled?: string | ((...args: any[]) => string)
isLeaf?: string | ((...args: any[]) => string)
class?: string | ((...args: any[]) => string)
}
renderAfterExpand?: boolean
load?: (...args: any[]) => Promise<any>
renderContent?: (...args: any[]) => JSX.Element | null
highlightCurrent?: boolean
defaultExpandAll?: boolean
expandOnClickNode?: boolean
checkOnClickNode?: boolean
autoExpandParent?: boolean
defaultExpandedKeys?: any[]
showCheckbox?: boolean
checkStrictly?: boolean
defaultCheckedKeys?: any[]
currentNodeKey?: string | number
filterNodeMethod?: (...args: any[]) => boolean
accordion?: boolean
indent?: number
icon?: string | ((...args: any[]) => JSX.Element | null)
lazy?: boolean
draggable?: boolean
allowDrag?: (...args: any[]) => boolean
allowDrop?: (...args: any[]) => boolean
on?: {
change?: (value: string | number | boolean | Object) => void
visibleChange?: (visible: boolean) => void
removeTag?: (tag: any) => void
clear?: () => void
blur?: (event: FocusEvent) => void
focus?: (event: FocusEvent) => void
nodeClick?: (...args: any[]) => void
nodeContextMenu?: (...args: any[]) => void
checkChange?: (...args: any[]) => void
check?: (...args: any[]) => void
currentChange?: (...args: any[]) => void
nodeExpand?: (...args: any[]) => void
nodeCollapse?: (...args: any[]) => void
nodeDragStart?: (...args: any[]) => void
nodeDragEnter?: (...args: any[]) => void
nodeDragLeave?: (...args: any[]) => void
nodeDragOver?: (...args: any[]) => void
nodeDragEnd?: (...args: any[]) => void
nodeDrop?: (...args: any[]) => void
}
slots?: {
default?: (...args: any[]) => JSX.Element | null
optionGroupDefault?: (item: SelectOption) => JSX.Element
optionDefault?: (option: SelectOption) => JSX.Element | null
prefix?: (...args: any[]) => JSX.Element | null
empty?: (...args: any[]) => JSX.Element | null
}
style?: CSSProperties
}
export interface FormSchema {
/**
* 唯一标识
*/
field: string
/**
* 表单字段类型
*/
fieldType?: 'string' | 'number' | 'array' | 'date' | undefined
/**
* 标题
*/
label?: string
/**
* col组件属性
*/
colProps?: ColProps
/**
* 表单组件属性,具体可以查看element-plus文档
*/
componentProps?:
| InputComponentProps
| AutocompleteComponentProps
| InputNumberComponentProps
| SelectComponentProps
| SelectV2ComponentProps
| CascaderComponentProps
| SwitchComponentProps
| RateComponentProps
| ColorPickerComponentProps
| TransferComponentProps
| RadioGroupComponentProps
| RadioButtonComponentProps
| DividerComponentProps
| DatePickerComponentProps
| DateTimePickerComponentProps
| TimePickerComponentProps
| InputPasswordComponentProps
| TreeSelectComponentProps
| UploadComponentProps
| JsonEditorProps
| any
/**
* formItem组件属性,具体可以查看element-plus文档
*/
formItemProps?: FormItemProps
/**
* 渲染的组件名称
*/
component?: ComponentName
/**
* 初始值
*/
value?: any
/**
* 是否隐藏,如果为true,会连同值一同删除,类似v-if
*/
remove?: boolean
/**
* 是否显示,如果为false,类似v-if
*/
visible?: boolean
/**
* 样式隐藏,不会把值一同删掉,类似v-show
*/
hidden?: boolean
/**
* @returns 远程加载下拉项
*/
optionApi?: any
}
export interface FormProps extends Partial<ElFormProps> {
schema?: FormSchema[]
isCol?: boolean
model?: Recordable
autoSetPlaceholder?: boolean
isCustom?: boolean
[key: string]: any
}
\ No newline at end of file
<template>
<div>
<DialogPlus modal-class="auth-user" append-to-body v-model="dialogVisible" @open="openedDialog" width="460px"
maxHeight="250px" :close-on-click-modal="false" title="用户身份认证">
<div class="select-tenant">
<el-form v-show="formType == 'validate'" label-position="top" ref="registerFormRef" :model="registerForm"
:rules="registerRules" class="login-form" auto-complete="on">
<el-form-item prop="logonUser" label="账号">
<el-input v-model.trim="registerForm.logonUser" :disabled="isModifypassword" placeholder="请输入账号" text tabindex="1" autocomplete="on">
</el-input>
</el-form-item>
<el-form-item prop="mobileNo" label="手机号">
<el-input v-model.trim="registerForm.mobileNo" :disabled="isModifypassword" placeholder="请输入手机号" text tabindex="1" autocomplete="on">
</el-input>
</el-form-item>
<el-form-item prop="validateCode" label="图形验证码">
<el-input class="captcha" v-model.trim="registerForm.validateCode" placeholder="请输入图形验证码" tabindex="2"
autocomplete="on">
<template #append>
<img class="h-26px" :src="imgCaptchaBase64" @click="refreshPictureCode" />
</template>
</el-input>
</el-form-item>
</el-form>
<el-form v-show="formType === 'reset'" label-position="top" ref="resetFormRef" :model="resetForm" :rules="resetRules"
autocomplete="off" class="login-form" auto-complete="on">
<el-form-item prop="smsCode" autocomplete="off" label="短信验证码">
<el-input v-model.trim="resetForm.smsCode" autocomplete="off" name="captcha-unique123"
:placeholder="`请输入${registerForm.mobileNo.substring(0, 3)}****${registerForm.mobileNo.substring(7)}收到的短信验证码`">
<template #append>
<span>{{ `${timeLeft}s` }}</span>
</template>
</el-input>
<div class="code-desc">
<span>若手机未收到验证码,请</span>
<el-button link type="primary" size="small" @click="recertification"
style="padding: 0px;vertical-align: baseline;">重新认证</el-button>
</div>
</el-form-item>
<el-form-item prop="pwd" label="新密码" :error="errorPsw">
<!-- <el-input v-model.trim="resetForm.pwd" type="password" placeholder="请输入新密码" show-password autocomplete="off"
autocomplete="new-password">
</el-input> -->
<PasswordStrengthMeter v-model.trim="password" placeholder="请输入新密码" @change="changePwd" style="width: 100%;"></PasswordStrengthMeter>
</el-form-item>
<el-form-item prop="checkPwd" label="确认新密码">
<el-input v-model.trim="resetForm.checkPwd" type="password" placeholder="请再次输入新密码" show-password
autocomplete="new-password">
</el-input>
</el-form-item>
</el-form>
</div>
<template #footer>
<el-button @click="closeDialog"> </el-button>
<el-button v-show="formType === 'validate'" :loading="sendCodeLoading" type="primary"
@click="handleSendCaptcha">发送验证码至手机</el-button>
<el-button v-show="formType === 'reset' && timeLeft > 0" :loading="saveLoading" type="primary"
:disabled="!resetForm.smsCode || !resetForm.pwd || !resetForm.checkPwd"
@click.prevent="handleReset">修改密码</el-button>
<el-button v-show="formType === 'reset' && timeLeft == 0" :loading="saveLoading" type="primary"
@click.prevent="recertification">重新认证</el-button>
</template>
</DialogPlus>
</div>
</template>
<script lang="ts" setup>
import { useValidator } from '@/hooks/useValidator';
import useCountdown from '@/hooks/useCountdown'
import type { FormRules } from 'element-plus'
import { propTypes } from '@/utils/propTypes'
import useIdaasStore from '@/store/modules/idaas';
import {
getPictureCode
} from "@/api/modules/idaas"
import useLogin from '@/store/modules/login'
import PasswordStrengthMeter from '../PasswordStrengthMeter/index.vue'
import { autoSalt } from '@/utils/common';
// const AsyncPasswordStrengthMeter = defineAsyncComponent(() =>
// import('../PasswordStrengthMeter/index.vue')
// );
const loginStore = useLogin()
const idaasStore = useIdaasStore()
const { required, phone } = useValidator();
const { proxy } = getCurrentInstance() as any;
const mobileNo = computed(()=> idaasStore.idaasUserInfo.principal?.mobileNo || '')
const logonUser = computed(()=> idaasStore.idaasUserInfo.principal?.logonUser || '')
const props = defineProps({
schemaInfo: {
type: Object as PropType<{
visible: boolean;
}>,
default: () => { }
},
operate:propTypes.string.def('modifypassword'),
});
const isModifypassword = computed(() => props.operate === 'modifypassword')
const dialogVisible = computed({
get: () => {
return props.schemaInfo.visible;
},
set: (val) => {
props.schemaInfo.visible = val
}
})
function openedDialog() {
recertification()
getPictureCodeInfo()
}
function closeDialog() {
dialogVisible.value = false
}
// 重新认证
const recertification = () => {
formType.value = 'validate';
clearForm()
reset()
}
function clearForm() {
let _mobileNo = isModifypassword.value ? mobileNo.value : ''
let _logonUser = isModifypassword.value ? logonUser.value : ''
password.value = ''
resetForm.value = {
smsCode: '',
pwd: '',
checkPwd: ''
}
registerForm.value = {
logonUser: _logonUser,
mobileNo: _mobileNo,
validateCode: ''
}
}
// ! 修改密码逻辑
const formType = ref('validate')
const resetFormRef = ref()
const registerFormRef = ref()
const registerRules = ref<FormRules>({
logonUser: [
{ required: true, trigger: 'blur', message: '请输入账号' },
],
mobileNo: [
required('请输入手机号'), phone()
],
validateCode: [
{ required: true, trigger: 'blur', message: '请输入图形验证码' },
]
})
const registerForm = ref({
logonUser: '',
mobileNo: '',
validateCode: ''
})
function changePwd() {
resetForm.value.pwd = password.value
}
const errorPsw = computed(() => loginStore.firstUnmetRequirement ? `需要${loginStore.firstUnmetRequirement}` : null)
const password = ref('')
/**
* 校验新密码
*/
function validatorPassword(rule, value, callback) {
console.log(value,loginStore.firstUnmetRequirement);
if (loginStore.firstUnmetRequirement) {
callback(new Error(`需要${loginStore.firstUnmetRequirement}`))
} else {
callback();
}
}
/**
* 校验确认密码
*/
function validatorConfirmpwd(rule, value, callback) {
if (value != resetForm.value.pwd) {
callback(new Error('密码不一致,请重新输入'))
} else {
callback();
}
}
const resetRules = ref<FormRules>({
smsCode: [
// 不显示验证信息了,只有填写完整,保存按钮才会被使用。
// {required: true, trigger: 'blur', message: '请输入收到的短信验证码' },
],
pwd: [
{
validator: validatorPassword,
trigger: []
}
],
checkPwd: [
{
validator: validatorConfirmpwd,
trigger: ['change', 'blur']
}
]
})
const resetForm = ref({
smsCode: '',
pwd: '',
checkPwd: ''
})
let promise: any = ref(null);
/** 图形验证码图片。 */
const imgCaptchaBase64 = ref('');
/** 图形验证码guid */
const validateCodeGuid = ref('');
/** 发送短信验证码的loading */
const sendCodeLoading = ref(false);
/** 重置密码保存的loading */
const saveLoading = ref(false);
const { timeLeft, minutes, seconds, start, stop, reset } = useCountdown(300);
watchEffect(() => {
console.log(timeLeft.value, 'timeLeft');
if (timeLeft.value == 0) {
stop()
}
})
const refreshPictureCode = () => {
if (promise.value) {
return;
}
getPictureCodeInfo()
}
function getPictureCodeInfo() {
promise.value = getPictureCode().then((res: any) => {
promise.value = null;
if (res.data.code == proxy.$passCode) {
imgCaptchaBase64.value = res.data.data?.imageBase64 || "";
validateCodeGuid.value = res.data.data?.guid || "";
}
});
}
const handleSendCaptcha = () => {
registerFormRef.value && registerFormRef.value.validate((valid) => {
if (valid) {
sendCodeLoading.value = true
let params: any = { ...registerForm.value };
params.validateCodeGuid = validateCodeGuid.value;
idaasApi.sendCode(params).then((res: any) => {
sendCodeLoading.value = false;
if (res?.data.code == proxy.$passCode) {
proxy.$ElMessage.success('验证码发送成功!');
loginStore.firstUnmetRequirement = ''
formType.value = 'reset';
resetForm.value.smsCode = '';
resetForm.value.pwd = "";
resetForm.value.checkPwd = "";
start()
} else {
refreshPictureCode();
registerForm.value.validateCode = "";
}
});
}
});
}
async function handleReset() {
let res = await resetFormRef.value?.validate()
if (!res) return
let params = Object.assign({}, resetForm.value, { mobileNo: registerForm.value.mobileNo, logonUser: registerForm.value.logonUser });
params.pwd = autoSalt(params.pwd, false, false);
idaasApi.resetPwd(params).then((res: any) => {
if (res.data.code == proxy.$passCode) {
proxy.$ElMessage.success('密码重置成功');
dialogVisible.value = false;
userApi.recordUpdateTime(registerForm.value.logonUser)
}
});
}
</script>
\ No newline at end of file
const lookup = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
const reverseLookup = new Uint8Array(256);
// 初始化 reverseLookup 数组
for (let i = 0; i < lookup.length; i++) {
reverseLookup[lookup.charCodeAt(i)] = i;
}
function decodeBase64url(base64url) {
var base64urlLength = base64url.length;
var placeHolderLength = base64url.charAt(base64urlLength - 2) === '=' ? 2 : base64url.charAt(base64urlLength - 1) === '=' ? 1 : 0;
var bufferLength = (base64urlLength * 3 / 4) - placeHolderLength;
var arrayBuffer = new ArrayBuffer(bufferLength);
var uint8Array = new Uint8Array(arrayBuffer);
var j = 0;
for (var i = 0; i < base64urlLength; i+=4) {
var tmp0 = reverseLookup[base64url.charCodeAt(i)];
var tmp1 = reverseLookup[base64url.charCodeAt(i+1)];
var tmp2 = reverseLookup[base64url.charCodeAt(i+2)];
var tmp3 = reverseLookup[base64url.charCodeAt(i+3)];
uint8Array[j++] = (tmp0 << 2) | (tmp1 >> 4);
uint8Array[j++] = ((tmp1 & 15) << 4) | (tmp2 >> 2);
uint8Array[j++] = ((tmp2 & 3) << 6) | (tmp3 & 63);
}
return arrayBuffer;
}
function encodeBase64url(arrayBuffer) {
var uint8Array = new Uint8Array(arrayBuffer);
var length = uint8Array.length;
var base64url = "";
for (var i = 0; i < length; i+=3) {
base64url += lookup[uint8Array[i] >> 2];
base64url += lookup[((uint8Array[i] & 3) << 4) | (uint8Array[i + 1] >> 4)];
base64url += lookup[((uint8Array[i + 1] & 15) << 2) | (uint8Array[i + 2] >> 6)];
base64url += lookup[uint8Array[i + 2] & 63];
}
switch (length % 3) {
case 1:
base64url = base64url.substring(0, base64url.length - 2);
break;
case 2:
base64url = base64url.substring(0, base64url.length - 1);
break;
}
return base64url;
}
// 导出函数
export default { decodeBase64url, encodeBase64url };
\ No newline at end of file
import { ref, computed } from 'vue';
/**
* 创建一个倒计时的自定义组合函数。
*
* @param {number} duration - 倒计时的初始秒数,默认为60秒。
* @returns {object} 包含倒计时状态和控制方法的对象。
*/
export default function useCountdown(duration = 60) {
// 使用 ref 创建响应式的倒计时剩余时间
const timeLeft = ref(duration);
// 用于存储定时器 ID 的变量
let intervalId = null;
// 计算属性,将剩余时间转换为分钟和秒数
const minutes = computed(() => Math.floor(timeLeft.value / 60));
const seconds = computed(() =>
('0' + (timeLeft.value % 60)).slice(-2) // 确保秒数始终显示两位数字
);
/**
* 开始倒计时。
*/
function start() {
if (!intervalId && timeLeft.value > 0) {
// 设置每秒减少一次剩余时间的定时器
intervalId = setInterval(() => {
if (timeLeft.value <= 0) {
clearInterval(intervalId); // 当时间用完时清除定时器
intervalId = null;
return;
}
timeLeft.value--; // 每秒减少一秒
}, 1000);
}
}
/**
* 停止倒计时。
*/
function stop() {
if (intervalId) {
clearInterval(intervalId); // 清除定时器以停止倒计时
intervalId = null;
}
}
/**
* 重置倒计时到初始值或指定的新持续时间。
*
* @param {number} [newDuration=duration] - 新的倒计时持续时间(秒),可选。
*/
function reset(newDuration = duration) {
stop(); // 先停止当前倒计时
timeLeft.value = newDuration; // 重置剩余时间为新的持续时间或默认持续时间
}
// 返回包含倒计时状态和控制方法的对象
return {
timeLeft, // 剩余时间(秒)
minutes, // 分钟
seconds, // 秒
start, // 开始
stop, // 暂停
reset // 重置
};
}
\ No newline at end of file
import type { FormPlus, FormExpose } from '@/components/FormPlus'
import type { ElForm, ElFormItem } from 'element-plus'
import { ref, unref, nextTick } from 'vue'
import { FormSchema, FormSetProps, FormProps } from '@/components/FormPlus'
import { isEmptyVal, isObject } from '@/utils/common'
import { cloneDeep } from 'lodash-es'
import { ComponentRef } from '@/types/global'
export const useForm = () => {
// From实例
const formRef = ref<typeof FormPlus & FormExpose>()
// ElForm实例
const elFormRef = ref<ComponentRef<typeof ElForm>>()
/**
* @param ref Form实例
* @param elRef ElForm实例
*/
const register = (ref: typeof FormPlus & FormExpose, elRef: ComponentRef<typeof ElForm>) => {
formRef.value = ref
elFormRef.value = elRef
}
const getForm = async () => {
await nextTick()
const form = unref(formRef)
if (!form) {
console.error('The form is not registered. Please use the register method to register')
}
return form
}
/**
* !数据类型空值映射
*/
const defaultEmptyValues = (fieldType:string | undefined):'' | any[] | null => {
let map = {
'string':'',
'number':'',
'array':[],
'date':null
}
return fieldType ? map[fieldType] : ''
}
// 一些内置的方法
const methods = {
/**
* @description 设置form组件的props
* @param props form组件的props
*/
setProps: async (props: FormProps = {}) => {
const form = await getForm()
form?.setProps(props)
if (props.model) {
form?.setValues(props.model)
}
},
/**
* @description 设置form的值
* @param data 需要设置的数据
*/
setValues: async (data: any) => {
const form = await getForm()
form?.setValues(data)
},
/**
* @description 设置schema
* @param schemaProps 需要设置的schemaProps
*/
setSchema: async (schemaProps: FormSetProps[]) => {
const form = await getForm()
form?.setSchema(schemaProps)
},
/**
* @description 新增schema
* @param formSchema 需要新增数据
* @param index 在哪里新增
*/
addSchema: async (formSchema: FormSchema, index?: number) => {
const form = await getForm()
form?.addSchema(formSchema, index)
},
/**
* @description 新增schema
* @param formSchemas 需要新增数据
* @param index 在哪里新增
*/
addSchemas: async (formSchemas: FormSchema[], index?: number) => {
const form = await getForm()
form?.addSchemas(formSchemas, index)
},
/**
* @description 删除schema
* @param field 删除哪个数据
* @param deleteField 是否删除字段
*/
delSchema: async (field: string,deleteField:boolean = true) => {
const form = await getForm()
form?.delSchema(field,deleteField)
},
/**
* @description 获取表单数据
* @returns form data
*/
getFormData: async (filterEmptyVal = true): Promise<any> => {
const form = await getForm()
const model = form?.formModel as any
if (filterEmptyVal) {
// 使用reduce过滤空值,并返回一个新对象
return Object.keys(model).reduce((prev, next) => {
const value = model[next]
if (!isEmptyVal(value)) {
if (isObject(value)) {
if (Object.keys(value).length > 0) {
prev[next] = value
}
} else {
prev[next] = value
}
}
return prev
}, {}) as T
} else {
return model as T
}
},
/**
* @description 获取表单组件的实例
* @param field 表单项唯一标识
* @returns component instance
*/
getComponentExpose: async (field: string) => {
const form = await getForm()
return form?.getComponentExpose(field)
},
/**
* @description 获取formItem组件的实例
* @param field 表单项唯一标识
* @returns formItem instance
*/
getFormItemExpose: async (field: string) => {
const form = await getForm()
return form?.getFormItemExpose(field) as ComponentRef<typeof ElFormItem>
},
/**
* @description 获取ElForm组件的实例
* @returns ElForm instance
*/
getElFormExpose: async () => {
await getForm()
return unref(elFormRef)
},
getFormExpose: async () => {
await getForm()
return unref(formRef)
},
/**
* 根据schema拿到表单的完整数据(如果字段为空则返回空字符串)
* @param resultForm
* @param schemaParam
* @returns
*/
getFormResult: (resultForm, schemaParam: FormSchema[]) => {
let res = {}
schemaParam.forEach(item => {
res[item.field] = isEmptyVal(resultForm[item.field]) ? defaultEmptyValues(item.fieldType) : resultForm[item.field]
})
return res
}
}
return {
formRegister: register,
formMethods: methods
}
}
/**
*
* @param reserveKeys 需要保留的key
* @param obj 需要删除的对象
* @description 删除对象中的key
*/
export const reserveField = (reserveKeys: string[],obj) => {
let copy = cloneDeep(obj)
for (const key in copy) {
if (reserveKeys.indexOf(key) === -1) {
delete copy[key]
}
}
return copy
}
import dayjs from '@/utils/dayjs'
import useUserStore from '@/store/modules/user'
import { getOrganisationRelTreeListPromise, getTemplateListPromise } from "@/api/modules/dataBasic"
const currentDate = dayjs(new Date()).format('YYYY-MM-DD')
const isArray = (val: any): val is Array<any> => {
return val && Array.isArray(val)
}
......@@ -15,81 +11,8 @@ const isNonEmptyArray = (val: any): boolean => {
const useGetData = (param = {}) => {
const BasicInfo: any = ref({}) // 基础资料
const gradeList = ref<any>([]) // 职级关系
const platformGradeList = ref<any>([]) // 平台职级
const postionList = ref<any>([]) // 职位
const tenantRelList = ref<any>([]) // 公司关系
const templateList = ref<any>([]) // 菜单模板列表
const orgMap = ref<orgMapRes>() // 人员组织信息
const organisationTree = ref<any>([]) // 组织树
const personelTree = ref<any>([]) // 人员树
const amoebaTree = ref<any>([]) // 阿米巴树
const financeSubjectDict = ref<any>([]) // 关联财务科目字典
const systemSideList = ref<{ // 系统列表
systemName: string,
guid: string
}[]>([])
const getFinanceSubject = async (customParam = {}) => { // 获取业务线
return await budgetApi.getFinanceSubjectTreePromise((Object.assign({}, param, customParam)))
}
// !基础数据缓存机制
async function getFinanceSubjectTree({ useCache = true, customParam = {} } = {}) {
if (isNonEmptyArray(BasicInfo.financeSubject) && useCache) {
return BasicInfo.financeSubject
} else {
let res = await getFinanceSubject(customParam)
BasicInfo.financeSubject = useCache ? res : []
return res
}
}
async function getGradeTitleRelList({ useCache = true, customParam = {} } = {}) { // 获取职级关系列表
if ( isNonEmptyArray(BasicInfo.gradeList) && useCache) {
gradeList.value = BasicInfo.gradeList
return gradeList.value
} else {
let res = await userApi.getGradeRelListPromise(customParam)
BasicInfo.gradeList = useCache ? res : []
gradeList.value = res
return gradeList.value
}
}
async function getGradeList({ useCache = true, customParam = {} } = {}) { // 获取平台职级列表
if ( isNonEmptyArray(BasicInfo.platformGradeList) && useCache) {
platformGradeList.value = BasicInfo.platformGradeList
return platformGradeList.value
} else {
let res = await userApi.getGradeListPromise(customParam)
BasicInfo.platformGradeList = useCache ? res : []
platformGradeList.value = res
return platformGradeList.value
}
}
async function getPostionList({ useCache = true, customParam = {} } = {}) { // 获取职位
if ( isNonEmptyArray(BasicInfo.postionList) && useCache) {
postionList.value = BasicInfo.postionList
return postionList.value
} else {
let res = await userApi.getPositionPromise(customParam)
BasicInfo.postionList = useCache ? res : []
postionList.value = res
return postionList.value
}
}
async function getTenantRelList(useCache = true) { // 获取公司关系列表
if (isNonEmptyArray(BasicInfo.tenantRelList) && useCache) {
tenantRelList.value = BasicInfo.tenantRelList
return tenantRelList.value
} else {
let res = await tenantApi.getTenantListPromise('Y')
BasicInfo.tenantRelList = useCache ? res : []
tenantRelList.value = res
return tenantRelList.value
}
}
async function getPermissionTemplateList({ useCache = true, customParam = {} } = {}) { // 获取菜单模板
if (isNonEmptyArray(templateList.value) && useCache) {
return templateList.value
......@@ -100,47 +23,6 @@ const useGetData = (param = {}) => {
}
}
interface orgMapRes {
[key:string]:{
guid:string, // staffGuid
userGuid:string, // userGuid
orgGuid:string, // 部门
orgName:string,
orgGuidTop:string, // 一级部门guid
orgNameTop:number,
}
}
// 获取人员组织信息
async function getOrgMap({ useCache = true, customParam = {} } = {}) { // 获取人员组织信息
const userStore = useUserStore()
let staffGuid = userStore.userInfo.staffGuid
if (isObject(BasicInfo.orgMap) && BasicInfo.orgMap[staffGuid] && useCache) {
orgMap.value = BasicInfo.orgMap
return orgMap.value
} else {
const userStore = useUserStore()
let res = await staffApi.getOrgMap([userStore.userInfo.staffGuid])
BasicInfo.orgMap = useCache ? res : null
orgMap.value = res
return orgMap.value
}
}
// 获取组织树
async function getPersonelTree({ useCache = true, customParam = {} } = {}) {
if ( isNonEmptyArray(BasicInfo.personelTree) && useCache) {
personelTree.value = BasicInfo.personelTree
return personelTree.value
} else {
const userStore = useUserStore()
let res = await tenantApi.getOrganisationTreePromise(userStore.userInfo.tenantGuid)
mulTreeData(res)
BasicInfo.personelTree = useCache ? res : []
personelTree.value = res
return personelTree.value
}
}
async function getOrganisationTree({ useCache = true, tenantGuid = '' } = {}) { // 获取组织树
if ( isNonEmptyArray(BasicInfo.organisationTree) && useCache) {
organisationTree.value = BasicInfo.organisationTree
......@@ -154,59 +36,9 @@ const useGetData = (param = {}) => {
}
}
async function getAmoebaTree({ useCache = true, customParam = {} } = {}) { // 获取组织树
if ( isNonEmptyArray(BasicInfo.amoebaTree) && useCache) {
amoebaTree.value = BasicInfo.amoebaTree
return amoebaTree.value
} else {
let res = await tenantApi.getAmoebaTreePromise2(currentDate)
getOrgtreeData(res,false)
BasicInfo.amoebaTree = useCache ? res : []
amoebaTree.value = res
return amoebaTree.value
}
}
async function getFinanceDict({ useCache = true, customParam = {} } = {}) { // 获取财务科目关联字典
if ( isNonEmptyArray(BasicInfo.financeSubjectDict) && useCache) {
financeSubjectDict.value = BasicInfo.financeSubjectDict
return financeSubjectDict.value
} else {
let res = await budgetApi.getSubjectDict(customParam)
getOrgtreeData(res,false)
BasicInfo.financeSubjectDict = useCache ? res : []
financeSubjectDict.value = res
return financeSubjectDict.value
}
}
// 获取子系统列表
async function getSystemSideList({ useCache = true, customParam = {} } = {}) {
if ( isNonEmptyArray(BasicInfo.systemSideList) && useCache) {
systemSideList.value = BasicInfo.systemSideList
return systemSideList.value
} else {
let res = await authApi.getSystemSideData()
BasicInfo.systemSideList = useCache ? res : []
systemSideList.value = res
return systemSideList.value
}
}
return {
getPersonelTree,
getFinanceSubject,
getGradeTitleRelList,
getGradeList,
getPostionList,
getTenantRelList,
getFinanceSubjectTree,
getPermissionTemplateList,
getOrgMap,
getOrganisationTree,
getAmoebaTree,
getFinanceDict,
getSystemSideList
}
}
......
......@@ -11,7 +11,6 @@ import useKeepAliveStore from '@/store/modules/keepAlive'
import useUserStore from '@/store/modules/user'
import useMenuStore from '@/store/modules/menu'
import useRouteStore from '@/store/modules/route'
import route from '@/mock/route'
const { isLoading } = useNProgress()
......@@ -125,19 +124,19 @@ router.beforeEach(async (to, from, next) => {
}
}
else {
if (to.name === 'home' || to.name == 'contactInfo' || to.name == 'register' || to.name == 'registerMobile' || to.name == 'homeDamRegister' || to.name == 'homeDamRegisterMobile' || to.name == 'homeDamFinance' || to.name == 'homeDamDataCircule' ||
if (to.name == 'userPrivate' || to.name == 'userAgree' || to.name === 'home' || to.name == 'contactInfo' || to.name == 'register' || to.name == 'registerMobile' || to.name == 'homeDamRegister' || to.name == 'homeDamRegisterMobile' || to.name == 'homeDamFinance' || to.name == 'homeDamDataCircule' ||
to.name == 'homeDamDemand' || to.name == 'homeDamAlgorithm' || to.name == 'homeDamAlgorithmMobile' || to.name == 'homeDamMarket' || to.name == 'homeDamMarketMobile'
|| to.name == 'homeDamDataCirculeMobile' || to.name == 'homeDamDemandMobile' || to.name == 'homeDamFinanceMobile') {
next()
}
else if (!to.query.code && to.name !== 'login') {
window.location.href = import.meta.env.VITE_IDASS_BASEURL;
// next({
// name: 'login',
// query: {
// redirect: to.fullPath !== '/' ? to.fullPath : undefined,
// },
// })
// window.location.href = import.meta.env.VITE_IDASS_BASEURL;
next({
name: 'login',
query: {
redirect: to.fullPath !== '/' ? to.fullPath : undefined,
},
})
}
else {
next()
......
......@@ -26,14 +26,30 @@ interface metaInfoRaw {
// 固定路由(默认路由)
const constantRoutes: RouteRecordRaw[] = [
// {
// path: '/login',
// name: 'login',
// component: () => import('@/views/login.vue'),
// meta: {
// title: '登录',
// },
// },
{
path: '/portalLogin',
name: 'login',
component: () => import('@/views/portal/portalLogin.vue'),
meta: {
title: '登录',
},
},
{
path: '/userPrivate',
name: 'userPrivate',
component: () => import('@/views/portal/userPrivate.vue'),
meta: {
title: '隐私声明',
},
},
{
path: '/userAgree',
name: 'userAgree',
component: () => import('@/views/portal/userAgree.vue'),
meta: {
title: '用户协议',
},
},
{
path: '/:all(.*)*',
name: 'notFound',
......
import { idaasLogin } from '@/api/modules/idaas';
import { autoSalt } from '@/utils/common';
import { ElMessage } from 'element-plus';
const useIdaas = defineStore(
// 唯一ID
'idaas',
() => {
const idaasToken = ref('') // idaas token
const isLoginOut = ref(false);// idaas 退出登录。
const idaasUserInfo = ref<{
principal?:{
logonUser:string,
mobileNo:string,
name:string
}
}>({})
// 登录
function login(data: any) {
data.username = data.logonUser;
data.password = autoSalt(data.password, false, false);
delete data.userType;
delete data.platformGuid;
delete data.logonUser;
data.needToastErr = 0;
data.telAreaCode = '+86';
isLoginOut.value = false;
return idaasLogin(data).then((res: any) => {
if (res?.code == '00000') {
// ElMessage.success('登录成功');
idaasUserInfo.value = res.data.data
return res.data;
} else {
ElMessage.error(res.msg)
}
})
}
return {
idaasToken,
idaasUserInfo,
login
}
},
{
persist:{
storage: localStorage,
paths: ['idaasToken','idaasUserInfo']
}
}
)
export default useIdaas
import { defineStore } from 'pinia'
const useLogin = defineStore(
// 唯一ID
'login',
() => {
const isCheckSms = ref(false)
const smsValidateCode = ref('');
const firstUnmetRequirement = ref('');
const encodePwd = ref('')
return {
/**
* 密文
*/
encodePwd,
/**
* 是否直接去校验验证码
*/
isCheckSms,
/**
* 网关登录验证码
*/
smsValidateCode,
/**
* 注册密码未满足的第一条规则的 label
*/
firstUnmetRequirement,
}
},
{
persist:{
storage: sessionStorage,
paths: ['firstUnmetRequirement','smsValidateCode','encodePwd']
}
}
)
export default useLogin
\ No newline at end of file
const sysConfigStore = defineStore(
// 唯一ID
'config',
() => {
let configMap: any = {};
// 封装请求配置文件的函数
const loadConfig = async () => {
try {
const response = await fetch('/config.json');
if (!response.ok) {
throw new Error(`请求配置失败,状态码: ${response.status}`);
}
const config = await response.json();
return config;
} catch (error) {
console.error('加载配置时出错:', error);
throw error;
}
};
const setConfig = (val) => {
configMap = val
}
const getConfig = (field) => {
if (import.meta.env.MODE == 'nginx' || import.meta.env.MODE == 'development') {
return import.meta.env.VITE_appKey
}
return field ? configMap[field] : configMap;
}
return {
configMap,
loadConfig,
setConfig,
getConfig
}
},
)
export default sysConfigStore
......@@ -21,6 +21,7 @@ declare module '@vue/runtime-core' {
Dialog_form: typeof import('./../components/Dialog/dialog_form.vue')['default']
Dialog_grid: typeof import('./../components/Dialog/dialog_grid.vue')['default']
Dialog_pane: typeof import('./../components/Dialog/dialog_pane.vue')['default']
DialogPlus: typeof import('./../components/DialogPlus/src/DialogPlus.vue')['default']
Drawer: typeof import('./../components/Drawer/index.vue')['default']
EchartsMap: typeof import('./../components/EchartsMap/index.vue')['default']
Editor: typeof import('./../components/Editor/src/Editor.vue')['default']
......@@ -28,13 +29,17 @@ declare module '@vue/runtime-core' {
FileUpload: typeof import('./../components/FileUpload/index.vue')['default']
FixedActionBar: typeof import('./../components/FixedActionBar/index.vue')['default']
Form: typeof import('./../components/Form/index.vue')['default']
FormItem: typeof import('./../components/FormItem/FormItem.vue')['default']
FormPlus: typeof import('./../components/FormPlus/src/FormPlus.vue')['default']
GraphTopbar: typeof import('./../components/RelationNetwork/graphTopbar.vue')['default']
Header: typeof import('./../components/Header/index.vue')['default']
Hour: typeof import('./../components/Schedule/component/hour.vue')['default']
ImagePreview: typeof import('./../components/ImagePreview/index.vue')['default']
ImagesUpload: typeof import('./../components/ImagesUpload/index.vue')['default']
ImageUpload: typeof import('./../components/ImageUpload/index.vue')['default']
LineageGraph: typeof import('./../components/LineageGraph/index.vue')['default']
ListPanel: typeof import('./../components/ListPanel/index.vue')['default']
Logo: typeof import('./../components/Logo/index.vue')['default']
LookBpmn: typeof import('./../components/ApprovalProcess/src/components/LookBpmn.vue')['default']
Month: typeof import('./../components/Schedule/component/month.vue')['default']
NotAllowed: typeof import('./../components/NotAllowed/index.vue')['default']
......@@ -46,6 +51,7 @@ declare module '@vue/runtime-core' {
PcasCascader: typeof import('./../components/PcasCascader/index.vue')['default']
Popover: typeof import('./../components/Popover/index.vue')['default']
RelationNetwork: typeof import('./../components/RelationNetwork/index.vue')['default']
Retrievepassword: typeof import('./../components/Retrievepassword/index.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
Schedule: typeof import('./../components/Schedule/index.vue')['default']
......
......@@ -7,6 +7,11 @@ type RecursivePartial<T> = {
[P in keyof T]?: RecursivePartial<T[P]>
}
declare type ComponentRef<T> = InstanceType<T>
declare type Recordable<T = any, K = string> = Record<K extends null | undefined ? string : K, T>
declare namespace Settings {
interface app {
/**
......
......@@ -1254,4 +1254,72 @@ export const isAllPropertiesEmpty = (obj,keyList?: string[]) => {
}
// 如果所有属性都有值,则返回true
return false;
}
\ No newline at end of file
}
/** 生成state的hash值 */
export const createStateHashCode = () => {
const array = new Uint32Array(1);
window.crypto.getRandomValues(array);
return array[0].toString(16) + Date.now();
}
/**
* 获取本地图
* @param name // 文件名 如 doc.png
* @returns {*|string}
*/
export const getAssetsImages = (name) => {
return new URL(`../assets/images/${name}`, import.meta.url).href;
}
export const blobToImageLink = (data):Promise<string> => {
return new Promise((resolve, reject) => {
let blob = new Blob([data],{type:'image/png'}); // #识别文件类型
let objectUrl = URL.createObjectURL(blob);
resolve(objectUrl)
})
}
export function isJsonString(str) {
if (typeof str !== 'string') return false;
// 忽略前导和尾随空白字符
str = str.trim();
// 检查字符串是否以 { 或 [ 开始,并以 } 或 ] 结束
if (!str.startsWith('{') && !str.startsWith('[')) return false;
if (!str.endsWith('}') && !str.endsWith(']')) return false;
try {
JSON.parse(str);
return true;
} catch (error) {
return false;
}
}
const toString = Object.prototype.toString
export const is = (val: unknown, type: string) => {
return toString.call(val) === `[object ${type}]`
}
export const isEmptyVal = (val: any): boolean => {
return val === '' || val === null || val === undefined
}
export const isObject = (val: any): val is Record<any, any> => {
return val !== null && is(val, 'Object')
}
/**
* 首字母大写
*/
export function firstUpperCase(str: string) {
return str.toLowerCase().replace(/( |^)[a-z]/g, (L) => L.toUpperCase())
}
/**
* 驼峰转横杠
*/
export const humpToDash = (str: string): string => {
return str.replace(/([A-Z])/g, '-$1').toLowerCase()
}
......
import { VueTypeValidableDef, VueTypesInterface, createTypes, toValidableType } from 'vue-types'
import { CSSProperties } from 'vue'
type PropTypes = VueTypesInterface & {
readonly style: VueTypeValidableDef<CSSProperties>
}
const newPropTypes = createTypes({
func: undefined,
bool: undefined,
string: undefined,
number: undefined,
object: undefined,
integer: undefined
}) as PropTypes
class propTypes extends newPropTypes {
static get style() {
return toValidableType('style', {
type: [String, Object]
})
}
}
export { propTypes }
import axios, {CancelTokenSource} from "axios";
import Storage from './composables/storage-helper';
import CryptoHelper from './composables/cryptoJs-helper';
import { ElMessage } from 'element-plus'
import useUserStore from '@/store/modules/user'
......@@ -13,9 +12,10 @@ interface Request {
source: CancelTokenSource;
}
const pendingRequests: Request[] = [];
const storage = new Storage();
const cryptoHelper = new CryptoHelper('cacheKey');
const IDaaSBaseURL = 'idaas'
const service = axios.create({
baseURL: (import.meta.env.VITE_OPEN_PROXY === 'true') ? `/api/` : `${import.meta.env.VITE_API_BASEURL}`,
timeout: 10 * 60 * 1000,// request timeout
......@@ -52,9 +52,15 @@ service.interceptors.request.use(
config.headers['real-ip'] = localStorage.getItem('ipAddress');
return config;
}
if (config.method === "idaasPost" || config.method === "idaaspost") {
config.baseURL = IDaaSBaseURL;
config.method = "post";
} else if (config.method === "idaasGet" || config.method === "idaasget") {
config.baseURL = IDaaSBaseURL;
config.method = "get";
}
if (config.responseType == "blob") {
// 文件流,文件名称相同时会判定同一个请求。
const userStore = useUserStore();
config.headers.Authorization = localStorage.getItem('token');
config.headers['real-ip'] = localStorage.getItem('ipAddress');
return config;
......
<script lang="ts" setup name="Header">
import Logo from '../Logo/index.vue';
import useSettingsStore from '@/store/modules/settings';
const { proxy } = getCurrentInstance() as any;
const settingsStore = useSettingsStore();
const currentPath = computed(() => proxy.$route.path)
onMounted(()=>{
})
</script>
<template>
<transition name="header">
<header v-if="settingsStore.mode === 'pc' && settingsStore.settings.menu.menuMode === 'head'">
<div class="header-container">
<Logo />
<!-- <Tools v-if="currentPath !== '/portalLogin'" /> -->
</div>
</header>
</transition>
</template>
<style lang="scss" scoped>
header {
// position: fixed;
// z-index: 1000;
top: 0;
left: 0;
right: 0;
display: flex;
align-items: center;
height: 64px;
color: var(--g-header-color);
// background: url('@/assets/images/header_bg.png') center/100% 100% no-repeat;
background-color: #fff;
transition: background-color 0.3s, var(--el-transition-color);
background-size: cover;
// position: relative;
.header-container {
width: var(--g-header-width);
height: 100%;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
}
.sidebar-collapse {
position: absolute;
left: calc(var(--g-sub-sidebar-width-portal) + 10px);
top: 50%;
transform: translateY(-50%);
z-index: 2;
cursor: pointer;
:deep(.el-icon) {
color: #fff;
}
}
.main {
width: calc(var(--g-header-width) - var(--g-sub-sidebar-width-portal) - 48px);
height: 100%;
padding-right: 24px;
}
@media screen and (max-width: var(--g-header-width)) {
.header-container {
width: 100%;
}
}
:deep(.title) {
position: relative;
width: calc(var(--g-sub-sidebar-width-portal) + 18px);
height: inherit;
padding-left: 18px;
padding-right: 0;
background-color: inherit;
flex-shrink: 0;
span {
font-size: 24px;
letter-spacing: 1px;
color: var(--g-header-color);
}
}
.nav {
display: flex;
width: 100%;
height: 100%;
padding: 0 25px;
align-items: center;
overflow-x: auto;
mask-image: linear-gradient(to right, transparent, #000 20px, #000 calc(100% - 20px), transparent);
// firefox隐藏滚动条
scrollbar-width: none;
// chrome隐藏滚动条
&::-webkit-scrollbar {
display: none;
}
.item-container {
position: relative;
display: flex;
width: initial;
height: inherit;
.item {
padding: 0 12px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
min-width: 60px;
height: 100%;
cursor: pointer;
color: var(--g-header-menu-color);
background-color: transparent;
// transition: background-color 0.3s, var(--el-transition-color);
&:hover {
color: var(--g-header-menu-hover-color);
background-color: var(--g-header-menu-hover-bg);
}
.el-icon {
font-size: 24px;
vertical-align: middle;
}
span {
text-align: center;
vertical-align: middle;
word-break: break-all;
@include text-overflow(1, false);
}
}
&.active .item {
color: var(--g-header-menu-active-color);
background-color: var(--g-header-menu-active-bg);
}
}
}
:deep(.tools) {
.buttons .item .el-icon {
color: var(--g-header-color);
}
.user-container {
font-size: 16px;
color: var(--g-header-color);
outline: none !important;
}
}
}
// 头部动画
.header-enter-active,
.header-leave-active {
transition: transform 0.3s;
}
.header-enter-from,
.header-leave-to {
transform: translateY(calc(var(--g-header-height-portal) * -1));
}
</style>
<script lang="ts" setup name="Logo">
import imgLogo from '@/assets/images/数据资产运营平台logo.png'
import useSettingsStore from '@/store/modules/settings'
defineProps({
showLogo: {
type: Boolean,
default: true,
},
showTitle: {
type: Boolean,
default: true,
},
})
const settingsStore = useSettingsStore()
const title = ref(import.meta.env.VITE_APP_TITLE)
const logo = ref(imgLogo)
const to = computed(() => {
const rtn: {
name?: string
} = {}
if (settingsStore.settings.home.enable) {
rtn.name = 'portal'
}
return rtn
})
</script>
<template>
<div class="title">
<img v-if="showLogo" :src="logo" class="logo">
<!-- <router-link :to="to" :class="{ 'is-link': settingsStore.settings.home.enable }" :title="title">
<img v-if="showLogo" :src="logo" class="logo">
</router-link>
<el-divider direction="vertical" /> -->
</div>
</template>
<style lang="scss" scoped>
.title {
position: fixed;
z-index: 1000;
top: 0;
width: inherit;
padding: 0 10px;
display: flex;
align-items: center;
justify-content: space-between;
height: var(--g-sidebar-logo-height);
text-align: center;
overflow: hidden;
text-decoration: none;
&.is-link {
cursor: pointer;
}
.logo {
width: 264px;
height: 64px;
object-fit: contain;
&+span {
margin-left: 10px;
}
}
span {
display: block;
font-weight: bold;
color: #fff;
@include text-overflow;
}
.el-divider {
height: 1.5rem;
margin-right: 0;
}
}
</style>
<template>
<div class="tools">
<div class="user-container" @click="enterUrl">
<div class="color-21 mark-icon">
<img :src="imgIcon" alt="" />
<span>{{ '知识库' }}</span>
</div>
</div>
<el-popover :width="280" :show-arrow="false" popper-class="user-setting">
<template #reference>
<div class="user-container">
<div class="user-wrapper">
<span class="max-w-120px p-r-5px">
<div class="nowrap-ellipsis text-right color-21"
style="color: #212121;font-weight: 600; line-height: 1.5;">
{{ userStore.userName || userData.staffName }}
</div>
<div class="nowrap-ellipsis text-right color-21" style="color: #666; font-size: 12px; line-height: 1.5;">
{{ userData.abbreviation }}
</div>
</span>
<el-avatar size="default">
<el-icon size="20px">
<svg-icon name="ep:user-filled" />
</el-icon>
</el-avatar>
</div>
</div>
</template>
<template #default>
<div class="avatar-info">
<div class='flex justify-start align-mid flex-items-center'>
<el-avatar :src="getAssetsImages('avatar.png')">
</el-avatar>
<div class="p-r-5px p-l-8px">
<div class="text-left font-600 font-size-18px color-21">
{{ userStore.userName || userData.staffName }}
</div>
<div class="text-left">
账号: {{ userData.mobileNo.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') }}
</div>
</div>
</div>
</div>
<div class="horizontal-line"></div>
<!-- <div class="user-setting-menu">
<div class="menu-wrapper" v-show="isNonEmptyArray(tenantInfoList) && tenantInfoList.length > 1"
@click="userCommand('changeTenant')">
<span class="color-21">切换公司</span>
<span>
<Icon icon="ep:arrow-right" :size='16' />
</span>
</div>
</div> -->
<!-- <div class="horizontal-line" v-show="isNonEmptyArray(tenantInfoList) && tenantInfoList.length > 1"></div> -->
<div class="user-setting-menu">
<div class="menu-wrapper" @click="userCommand('updatePwd')">
<span class="color-21">修改密码</span>
<span>
<Icon icon="ep:arrow-right" :size='16' />
</span>
</div>
</div>
<div class="horizontal-line"></div>
<div class="user-setting-menu">
<div class="menu-wrapper" @click="userCommand('stopUser')">
<span class="color-21">注销登录</span>
<span>
<Icon icon="ep:arrow-right" :size='16' />
</span>
</div>
</div>
<template v-if="bindList && bindList.length">
<div class="horizontal-line"></div>
<div class="user-setting-menu">
<div class="menu-wrapper" @click="userCommand('passKeyUnbind')">
<span class="color-21">通行密钥解绑</span>
<span>
<Icon icon="ep:arrow-right" :size='16' />
</span>
</div>
</div>
</template>
<div class="horizontal-line"></div>
<div class="login-out">
<div class="login-out-btn" @click="userCommand('logout')">
{{ '退出登录' }}
</div>
</div>
</template>
</el-popover>
<Retrievepassword :schemaInfo="retrievepassword"></Retrievepassword>
<!-- <ChangeTenant :schemaInfo="schemaInfo" @confirm="selectSure" @close="() => { schemaInfo.visible = false; }">
</ChangeTenant> -->
</div>
</template>
<script lang="ts" setup name="Tools">
import { ElMessageBox } from 'element-plus';
import Retrievepassword from '../Retrievepassword/index.vue'
import useUserStore from '@/store/modules/user'
import imgIcon from '@/assets/images/知识库.png'
import { getAssetsImages, isNonEmptyArray } from '@/utils/common';
const userStore = useUserStore();
const userData = JSON.parse(userStore.userData)
const { proxy } = getCurrentInstance() as any;
const router = useRouter()
// ! 修改密码逻辑
const retrievepassword = ref({
visible: false,
})
const bindList = ref([])
const enterUrl = () => {
window.open('https://data-assets.zgene.cn/1zcdj');
}
function userCommand(command: 'logout' | 'updatePwd' | 'passKeyUnbind' | 'stopUser') {
switch (command) {
case 'updatePwd':
retrievepassword.value.visible = true
break
case 'logout':
logout()
break
case 'stopUser':
stopLogonUser()
break
case 'passKeyUnbind':
ElMessageBox.confirm('确定解绑吗, 是否继续?', "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: 'warning',
}).then(() => {
// TODO
// let list: any = [];
// bindList.value.forEach(item => {
// list.push(item.guid)
// })
// webAuth.unbindWebAuths(list).then((respones: any) => {
// console.log(respones)
// if (respones.data.code == '00000') {
// bindList.value = [];
// ElMessage({
// type: "success",
// message: "解绑成功!",
// });
// }
// })
})
}
}
function logout() {
// proxy.$openMessageBox("是否确认退出登录", "warning", () => {
// userApi.logout().then(() => {
// userStore.logout({
// logMessage: 'logout-常触发退出登录'
// })
// localStorage.removeItem("token");
// localStorage.removeItem("refreshToken");
// localStorage.removeItem("tokenTimestamp");
// localStorage.removeItem("user");
// proxy.$router.push('/portalLogin')
// })
// })
}
/** 注销账号 */
function stopLogonUser() {
// proxy.$openMessageBox(userData?.isAdmin === 'Y' ? "是否确认注销当前管理员用户及其所在企业下所有的用户?" : "是否确认注销当前用户?", "warning", () => {
// staffInfoApi.stopLogonUser(userData?.staffGuid).then((res) => {
// if (res.data.code == '00000') {
// ElMessage({
// type: "success",
// message: "注销账号成功",
// });
// userApi.logout().then(() => {
// userStore.logout({
// logMessage: 'logout-常触发退出登录'
// })
// localStorage.removeItem("token");
// localStorage.removeItem("refreshToken");
// localStorage.removeItem("tokenTimestamp");
// localStorage.removeItem("user");
// proxy.$router.push('/portalLogin')
// })
// } else {
// ElMessage({
// type: "error",
// message: "注销账号失败",
// });
// }
// });
// })
}
onMounted(() => {
// webAuth.getCurrentUserWebAuths().then((res: any) => {
// let list = res.data;
// if (list && list.length) {
// bindList.value = list;
// }
// })
});
</script>
<style lang="scss" scoped>
:deep(.el-popper) {
padding: 0;
}
:deep(.user-setting) {
padding: 0 !important;
}
:deep(.el-radio-group) {
.el-radio--large {
width: 420px;
}
}
.tools {
display: flex;
align-items: center;
white-space: nowrap;
.buttons {
.item {
display: inline-flex;
align-items: center;
justify-content: center;
height: 24px;
width: 34px;
cursor: pointer;
vertical-align: middle;
.el-icon {
color: var(--el-text-color-primary);
transition: var(--el-transition-color);
}
}
.item-pro {
display: inline-flex;
align-items: center;
width: auto;
padding: 0 10px;
transform-origin: right center;
animation: pro-text 3s ease-out infinite;
@keyframes pro-text {
0%,
20% {
transform: scale(1);
}
50%,
70% {
transform: scale(1.2);
}
100% {
transform: scale(1);
}
}
.title {
padding-left: 5px;
font-weight: bold;
font-size: 14px;
background-image: linear-gradient(to right, #ffa237, #fc455d);
background-clip: text;
-webkit-text-fill-color: transparent;
}
}
}
}
.avatar-info {
padding-bottom: 10px;
}
:deep(.user-container) {
display: inline-block;
cursor: pointer;
height: var(--g-header-height-portal);
// line-height: var(--g-header-height-portal);
display: flex;
align-items: center;
padding: 0;
.user-wrapper {
display: flex;
align-items: center;
padding-left: 12px;
padding-right: 24px;
.el-avatar {
vertical-align: middle;
margin-top: -2px;
}
}
}
.user-container:hover {
background: #f6f6f6;
}
.segmentation {
display: flex;
justify-content: center;
/* 水平居中 */
align-items: center;
/* 垂直居中 */
height: var(--g-header-height-portal);
/* 容器的高度为视口高度 */
}
.vertical-line {
width: 1px;
/* 分割线的宽度 */
height: 70%;
/* 分割线的高度 */
background-color: #fff;
/* 白色背景 */
}
.horizontal-line {
border: none;
height: 1px;
background-color: #E5E5E5;
width: 100%;
/* 宽度为容器的100% */
}
.menu-wrapper {
cursor: pointer;
display: flex;
justify-content: space-between;
padding: 10px 0;
}
.menu-wrapper:hover {
background: #F5F5F5;
}
.login-out {
cursor: pointer;
text-align: center;
padding: 0 10px;
}
.login-out-btn {
height: 32px;
line-height: 32px;
margin-top: 10px;
width: 100%;
background: #FFFFFF;
border: 1px solid rgba(217, 217, 217, 1);
}
.color-21 {
color: #212121;
font-size: 14px;
padding-right: 12px;
&.mark-icon {
padding: 0 16px;
display: flex;
align-items: center;
position: relative;
img {
width: 24px;
height: 24px;
margin-right: 8px;
}
&::after {
height: 40px;
content: '';
border-right: 1px solid #E5E5E5;
position: absolute;
top: 50%;
right: 0;
transform: translateY(-50%);
}
}
}
:deep(.el-input.captcha) {
.el-input__wrapper {
border-radius: var(--el-input-border-radius, var(--el-border-radius-base));
}
.el-input-group__append {
width: 150px;
margin-left: 8px;
box-shadow: 0 0 0 1px var(--el-input-border-color, var(--el-border-color)) inset;
border-radius: var(--el-input-border-radius, var(--el-border-radius-base));
}
img {
cursor: pointer;
}
}
</style>
<template>
<div class="main-h">
<Header />
<div class="bg-banner">
<div id="login-box" >
<div :class="['login_form', (formType === 'login'||formType === 'beforeLogin'|| formType === 'loginByPassKey'||formType === 'resignByPassKey') ? 'login-size' : 'register-size']" :style="formType === 'loginByPassKey'?'height:280px;z-index: 1001':'z-index: 1001'">
<!-- <img v-if="formType !== 'loginByPassKey'&&formType !== 'resignByPassKey'&&formType !== 'login'" :src="imgSrc" @click="switchHandle" class="login-img"> -->
<div class="login-title" :style="{ marginBottom: loginTitle == '通行密钥登录' ? '30px' : '0px' }">{{ loginTitle }}</div>
<!-- 登录密码验证 -->
<FormItem class="pt-30px" v-if="formType === 'beforeLogin'||formType==='resignByPassKey'" ref="beforeLoginFormRef" :schemaParam="beforeLoginSchema" />
<!-- 正式登录 -->
<FormItem class="pt-30px" v-if="formType === 'login'" ref="loginFormRef" :schemaParam="loginSchema" />
<!-- 注册表单 -->
<FormItem class="pt-30px" v-if="formType === 'register'" ref="registerForm"
:schemaParam="registerSchema" />
<div class="xieyi">
<el-checkbox v-model="isAgree">我已阅读并同意<span style="color: #4fa1a4"
@click.stop.prevent="newOpen(0)">《隐私声明》</span><span style="color: #4fa1a4"
@click.stop.prevent="newOpen(1)">《用户协议》</span></el-checkbox>
</div>
<!-- before登录 -->
<div v-if="formType === 'beforeLogin'" class="login-footer">
<el-button class="loginButton" :loading="loading" :disabled="!isAgree" ref="loginButton" type="primary" size="large" style="width: 100%;" @click.prevent="beforeLogin">
登录
</el-button>
<div class="flex" style="justify-content: space-between;">
<el-button class="forget-pwd-btn" link size="small" @click.prevent="formType='loginByPassKey'">
通行密钥登录
</el-button>
<el-button class="forget-pwd-btn" link size="small" @click.prevent="retrievePassword">
忘记密码
</el-button>
</div>
</div>
<!-- 继续登录 -->
<div v-if="formType === 'login'" class="login-footer">
<el-button class="loginButton" :loading="loading" ref="loginButton" type="primary" size="large" style="width: 100%;" @click.prevent="verifyUser">
继续登录
</el-button>
<div class="flex" style="justify-content: space-between;">
<el-button class="forget-pwd-btn" link size="small" @click.prevent="formType='loginByPassKey'">
通行密钥登录
</el-button>
<el-button class="forget-pwd-btn" link size="small" @click.prevent="retrievePassword">
忘记密码
</el-button>
</div>
</div>
<!--通行密钥登录 -->
<div v-if="formType === 'loginByPassKey'" class="login-footer">
<el-button class="loginButton" :loading="passKeyLoginloading" ref="loginButton" :disabled="!isAgree" type="primary" size="large" style="width: 100%;" @click.prevent="passKeyLogin">
登录
</el-button>
<div class="flex" style="justify-content: space-between;">
<el-button class="forget-pwd-btn" link size="small" @click.prevent="formType='beforeLogin'">
账号密码登录
</el-button>
<el-button class="forget-pwd-btn" link size="small" @click.prevent="formType='resignByPassKey'">
去注册
</el-button>
</div>
</div>
<!-- 通行密钥登录 -->
<!--通行密钥注册 -->
<div v-if="formType === 'resignByPassKey'" class="login-footer" >
<el-button class="loginButton" :loading="passKeyRegisterloading" ref="loginButton" type="primary" size="large" style="width: 100%;" @click.prevent="passKeySign">
注册
</el-button>
<el-button class="forget-pwd-btn" link size="small" @click.prevent="formType='loginByPassKey'">
去登录
</el-button>
</div>
<!-- 通行密钥注册 -->
<!-- <div v-if="formType === 'register'" class="login-footer">
<el-button :loading="loading" :disabled="!isAgree" type="primary" size="large" style="width: 100%;" @click.prevent="handleRegister">
注册
</el-button>
</div> -->
</div>
</div>
<div class="copyright_text">
<span>Copyright © 2015-2024</span>
<a style="color: #4FA1A4;margin: 0 8px;" href="https://beian.miit.gov.cn" target="_blank">京ICP备2024044205号</a>
<span>北京传世博润科技有限公司</span>
</div>
</div>
</div>
<!-- <el-dialog v-model="centerDialogVisible" width="360" title="图形验证码" align-center>
<template #default>
<div class="img-code">
<el-form ref="ruleFormRef" :model="ruleForm" :rules="rules" class="demo-ruleForm" @submit.native.prevent>
<el-form-item>
<div class="img_code">
<img :src="imgCodePath" />
<span>看不清?<span class="text_btn" @click="getImgCode">换一张</span></span>
</div>
</el-form-item>
<el-form-item prop="captcha">
<el-input v-model="ruleForm.captcha" placeholder="请输入图形验证码" clearable @keyup.enter="sendCode" />
</el-form-item>
</el-form>
</div>
</template>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" size="large" @click="sendCode">确定</el-button>
</div>
</template>
</el-dialog> -->
<AsyncRetrievepassword :schemaInfo="retrievepassword" :operate="operate"></AsyncRetrievepassword>>
<!-- 图形验证码 -->
<!-- <DialogPlus modal-class="auth-user" append-to-body v-model="imgCheckDialog" width="460px"
maxHeight="150px" :close-on-click-modal="false" title="图形验证">
<div class="select-tenant">
<el-form label-position="top" ref="pictureFormRef" :model="pictureFormData"
:rules="pictureRules" class="login-form" auto-complete="on">
<el-form-item prop="validateCode" label="图形验证码">
<el-input class="captcha" v-model.trim="pictureFormData.validateCode" placeholder="请输入图形验证码" tabindex="2"
autocomplete="on">
<template #append>
<img class="h-26px" :src="imgCaptchaBase64" @click="refreshPictureCode" />
</template>
</el-input>
</el-form-item>
</el-form>
</div>
<template #footer>
<el-button @click="()=>{imgCheckDialog=false}">取 消</el-button>
<el-button :loading="sendCodeLoading" type="primary"
@click="checkPictureCode">发送验证码至手机</el-button>
</template>
</DialogPlus> -->
</template>
<script lang="tsx" setup name="portalLogin">
import { defineAsyncComponent } from 'vue';
import Header from './components/Header/index.vue'
import useUserStore from '@/store/modules/user';
import useIdaasStore from '@/store/modules/idaas';
import sysConfigStore from '@/store/modules/sysConfig'
import { useValidator } from '@/hooks/useValidator'
import { FormSchema } from '@/components/FormPlus'
import useCountdown from '@/hooks/useCountdown'
import { ElMessageBox } from 'element-plus'
import {
checkLoginUser,
checkDeviceTypeRegist,
signUp,
} from '@/api/modules/idaas';
import type { FormRules } from 'element-plus'
import { v4 as uuidv4 } from 'uuid';
import Base64url from '@/hooks/base64url'
import md5 from 'md5';
import useLogin from '@/store/modules/login'
import CryptoJS from 'crypto-js'
import { createStateHashCode, getAssetsImages, blobToImageLink, isJsonString } from '@/utils/common';
import {
getLoginWebAuthn, sendLoginCode, checkImgCode, registWebAuthn, getWebAuth4jLogin
} from '@/api/modules/idaas'
const AsyncPasswordStrengthMeter = defineAsyncComponent(() =>
import('../../components/PasswordStrengthMeter/index.vue')
);
const AsyncRetrievepassword = defineAsyncComponent(() =>
import('../../components/Retrievepassword/index.vue')
);
const loginStore = useLogin()
const { required, phone, isUSCCCode } = useValidator();
const { proxy } = getCurrentInstance() as any;
const route = useRoute();
const loginButton = ref()
const isAgree = ref(false);
/** 打开用户阅读协议 */
function newOpen(num) {
if (num == 0) {
window.open(window.location.origin + '/userPrivate')
} else {
window.open(window.location.origin + '/userAgree')
}
}
// ! 忘记密码
const retrievepassword = ref({
visible: false
})
// retrievepassword(忘记密码) modifypassword(修改密码)
const operate = ref('retrievepassword')
/**
* 去修改密码
* @param param0
*/
function changePassword({
mobileNo,
logonUser,
name
}) {
idaasStore.idaasUserInfo = {
principal: {
mobileNo: mobileNo,
logonUser: logonUser,
name: name
}
}
// 去修改逻辑
retrievePwdHanlder('modifypassword')
}
/**
* 忘记密码
*/
async function retrievePassword() {
if (logonUser.value) {
let res: any = await checkLoginUser(logonUser.value);
if (res?.code == '00000') {
let checkRes = res.data || {};
if (checkRes) {
changePassword({
mobileNo: checkRes.mobileNo,
logonUser: checkRes.logonUser,
name: checkRes.userName
})
} else {
retrievePwdHanlder()
}
} else {
res?.msg && proxy.$ElMessage.error(res.msg);
}
} else {
let formData = await beforeLoginFormRef.value.getData();
if (formData.logonUser) {
logonUser.value = formData.logonUser;
retrievePassword()
} else {
retrievePwdHanlder()
}
}
logonUser.value = ''
}
/**
* 找回密码处理函数
* @param type
*/
function retrievePwdHanlder(type = 'retrievepassword') {
retrievepassword.value.visible = true;
operate.value = type
}
const formTypeMap = {
resignByPassKey: '通行密钥注册',
loginByPassKey: '通行密钥登录',
beforeLogin: '手机号登录',
login: '登录验证',
// 默认情况(比如 register 或其他)映射到 '注册申请'
};
const loginTitle = computed(() => {
return formTypeMap[formType.value] || '注册申请';
});
const formType = ref(proxy.$route.query.formType || 'beforeLogin');
const beforeLoginFormRef = ref() // 登录前表单
const loginFormRef = ref() // 登录表单
const registerForm = ref() // 注册表单
const pictureFormRef = ref() //
const pictureFormData = ref<any>({}) // 注册表单
const pictureRules = ref<FormRules>({
validateCode: [
{ required: true, trigger: 'blur', message: '请输入图形验证码' },
{
// 图形验证码校验
validator: (rule: any, value: any, callback: any) => {
if (value && vCode.value && value !== vCode.value) {
callback(new Error('验证失败'));
} else if (value && vCode.value) {
callback();
}
callback();
},
trigger: "change",
}
]
})
/** 图形验证码图片。 */
const imgCaptchaBase64 = ref('');
const vCode = ref(''); // 图片校验的编码
// 登录表单
const beforeLoginSchema = reactive<FormSchema[]>([
{
field: 'logonUser',
label: '请输入手机号/账号',
component: 'Input',
colProps: {
span: 24
},
componentProps: {},
formItemProps: {
rules: [required()], // 校验规则
}
},
{
field: 'password',
label: '请输入密码',
component: 'Input',
colProps: {
span: 24
},
componentProps: {
showPassword: true,
autocomplete: 'current-password'
},
formItemProps: {
rules: [required()], // 校验规则
}
},
])
// 登录表单
const loginSchema = reactive<FormSchema[]>([
{
field: 'mobileNo',
label: '验证手机号',
component: 'Input',
colProps: {
span: 24
},
componentProps: {
disabled: true,
readonly: true,
formatter: (value: string) => {
if (!value) return '';
const str = String(value); // 确保是字符串
if (str.length < 7) return str; // 长度不够不处理
return str.substring(0, 3) + '****' + str.substring(7);
}
},
formItemProps: {
rules: [required()], // 校验规则
}
},
{
field: 'smsCode',
label: '短信验证码',
component: 'Input',
colProps: {
span: 24
},
componentProps: {
slots: {
append: () => {
return (
<>
{
sendCodeLoading2.value ?
<span class="w-80px text-center cursor-not-allowed fontC-4fa1a4">{`重新获取(${timeLeft2.value})s`}</span>
:
<span class="text-center cursor-pointer fontC-4fa1a4" v-show={!sendCodeLoading2.value} onClick={() => {
getLoginSmsCode()
}}>{sendCodeText.value}</span>
}
</>
)
}
}
},
formItemProps: {
rules: [
{
validator: (rule: any, value: any, callback: any) => {
if (disabledverifyUser.value) {
callback(new Error('请先获取验证码'));
}
callback()
},
trigger: ["change","blur"],
},
required(),
], // 校验规则
}
},
])
const switchHandle = () => {
formType.value = formType.value === 'beforeLogin' ? 'register' : 'beforeLogin';
clearFormData()
}
// 重置表单
function clearFormData() {
password.value = ''
beforeLoginFormRef.value.setValue({}, true);
registerForm.value.setValue({}, true);
}
/**
* 校验注册密码
*/
function validatorPassword(rule, value, callback) {
if (!password.value) {
callback(new Error('该项为必填项'))
} else if (loginStore.firstUnmetRequirement) {
callback(new Error(`需要${loginStore.firstUnmetRequirement}`))
} else {
callback();
}
}
/**
* 校验确认密码
*/
function validatorConfirmpwd(rule, value, callback) {
if (value != password.value) {
callback(new Error('密码不一致,请重新输入'))
} else {
callback();
}
}
// 注册表单
const password = ref('')
const registerSchema = reactive<FormSchema[]>([
{
field: 'tenantName',
label: '企业名称',
component: 'Input',
colProps: {
span: 24
},
componentProps: {
maxlength: 100,
},
formItemProps: {
rules: [required()], // 校验规则
}
},
{
field: 'tenantCode',
label: '统一社会信用代码',
component: 'Input',
colProps: {
span: 12
},
componentProps: {
maxlength: 200,
},
formItemProps: {
rules: [required(), isUSCCCode('统一社会信用代码格式不正确')], // 校验规则
}
},
{
field: 'userName',
label: '账号',
component: 'Input',
colProps: {
span: 12
},
componentProps: {
maxlength: 20,
},
formItemProps: {
rules: [required(),phone(), /* beforeRegisterCheck('name')*/], // 校验规则
}
},
{
field: 'password',
label: '登录密码',
component: 'Input',
colProps: {
span: 12
},
componentProps: {
maxlength: 20,
type: 'password',
showPassword: true,
autocomplete : 'new-password'
},
formItemProps: {
// 校验规则
rules: [
required(),
{
validator: validatorPassword,
trigger: ['change','blur']
},
],
slots: {
default: () => {
return (
<>
<AsyncPasswordStrengthMeter v-model={password.value} onChange={() => {
registerForm.value.setValue({
password: password.value
})
registerForm.value.formValidation(['password'])
}}/>
</>
)
}
}
}
},
{
field: 'confirmpwd',
label: '确认登录密码',
component: 'Input',
colProps: {
span: 12
},
componentProps: {
placeholder:'请输入密码',
maxlength: 20,
type: 'password',
showPassword: true,
clearable: false,
autocomplete : 'new-password'
},
formItemProps: {
// 校验规则
rules: [
required(),
{
validator: validatorConfirmpwd,
trigger: ['change','blur']
},
],
}
},
{
field: 'mobileNo',
label: '联系方式',
component: 'Input',
colProps: {
span: 24
},
componentProps: {
maxlength: 20,
},
formItemProps: {
rules: [required(), phone(), /*beforeRegisterCheck('mobileNo')*/], // 校验规则
}
},
{
field: 'smsCode',
label: '短信验证码',
component: 'Input',
colProps: {
span: 24
},
componentProps: {
slots: {
append: () => {
return (
<>
{
sendCodeLoading.value ?
<span class="w-80px text-center fontC-4fa1a4">{`重新获取(${timeLeft.value})s`}</span>
:
<span class="text-center cursor-pointer fontC-4fa1a4" v-show={!sendCodeLoading.value} onClick={() => {
// getRegisterImgCode()
}}>{'获取短信验证码'}</span>
}
</>
)
}
}
},
formItemProps: {
rules: [required()], // 校验规则
}
},
])
const userStore = useUserStore()
const idaasStore = useIdaasStore();
/** 登录按钮的loading */
const loading = ref(false)
/** 发送短信验证码的loading */
const sendCodeLoading = ref(false);
const sendCodeLoading2 = ref(false);
const sendCodeText = ref('获取短信验证码');
const disabledverifyUser = ref(true)
/** 重置密码保存的loading */
const saveLoading = ref(false);
// ! 图形验证
const centerDialogVisible = ref(false);
const imgCodePath = ref('')
const imgCodeGuid = ref('');
const ruleFormRef = ref();
const ruleForm = ref({
captcha: ''
})
const rules = ref({
captcha: [
{
validator: (rule: any, value: any, callback: any) => {
if (value === '') {
callback(new Error('请填写图形验证码'))
} else {
let params: any = {
captcha: value,
captchaGuid: imgCodeGuid.value
};
checkImgCode(params).then((res: any) => {
if (res.code == proxy.$passCode) {
if (!res.data) {
callback(new Error("验证码错误,请重新填写"));
} else {
callback();
}
} else {
callback(new Error(res.msg));
}
}).catch((xhr) => {
callback(new Error(xhr.msg));
});
}
}, trigger: 'blur'
},
{
min: 1,
message: '请填写图形验证码',
trigger: 'change',
},
]
})
// const getImgCode = () => {
// getImgCodeSrc({ width: 180, height: 40 }).then((res) => {
// imgCodeGuid.value = res.data.data?.guid || '';
// imgCodePath.value = res.data.data?.imageBase64 || '';
// if (!centerDialogVisible.value) {
// ruleForm.value.captcha = '';
// centerDialogVisible.value = true;
// nextTick(() => {
// ruleFormRef.value.clearValidate();
// })
// }
// })
// }
const { timeLeft, minutes, seconds, start, stop, reset } = useCountdown(60);
watchEffect(() => {
if (timeLeft.value == 0) {
reset()
sendCodeLoading.value = false
}
})
// 身份验证短信获取倒计时
const { timeLeft:timeLeft2, start:start2, reset:reset2 } = useCountdown(60);
watchEffect(() => {
if (timeLeft2.value == 0) {
reset2()
sendCodeText.value = '再次发送'
sendCodeLoading2.value = false
}
})
/**
* 手机号身份验证
*/
async function getLoginSmsCode() {
let validate = await loginFormRef.value.formValidation(['mobileNo']);
if (validate) {
let params = await loginFormRef.value.getData()
sendLoginCode(params.mobileNo).then(res=>{
if (res.data.code == proxy.$passCode) {
sendCodeLoading2.value = true;
disabledverifyUser.value = false;
loginFormRef.value.formValidation(['smsCode']);
start2()
}
});
}
}
// // 获取注册的图片验证
// const getRegisterImgCode = async () => {
// let validate = await registerForm.value.formValidation(['mobileNo'])
// if (validate) {
// imgCheckDialog.value = true;
// pictureFormData.value.validateCode = null;
// await nextTick();
// getCommoncheckImgCode()
// }
// }
// function getCommoncheckImgCode() {
// commoncheckImgCode().then(async (res) => {
// if (res.status == 200) {
// vCode.value = res.headers['v-code'] || ''
// imgCaptchaBase64.value = await blobToImageLink(res.data)
// console.log(imgCaptchaBase64.value, vCode.value, 'imgCaptchaBase64');
// } else {
// vCode.value = ''
// imgCaptchaBase64.value = ''
// }
// })
// }
// async function refreshPictureCode() {
// getCommoncheckImgCode()
// }
// async function checkPictureCode() {
// let validate = await pictureFormRef.value.validate()
// if (validate) {
// await getSmsCode2();
// imgCheckDialog.value = false;
// }
// }
// const getSmsCode2 = async () => {
// let validate = await registerForm.value.formValidation(['mobileNo'])
// if (validate) {
// let params = await registerForm.value.getData()
// userApi.sendRegisterCode(params.mobileNo);
// sendCodeLoading.value = true;
// start()
// }
// }
// const sendCode = () => {
// const formEl = ruleFormRef.value;
// if (!formEl) return;
// formEl.validate((valid, fields) => {
// if (valid) {
// centerDialogVisible.value = false;
// handleRegister();
// }
// })
// }
// ! 图形验证弹框
const imgCheckDialog = ref(false)
let LOGINCODE = {
grant_type: 'authorization_code',
response_type: 'code',
client_id: sysConfigStore().getConfig('appKey'),
scope: 'other',
state: 'authorization-life',
loginRedirectUrl: window.location.origin // 登录重定向域名
}
const mobileNo = ref('')
const logonUser = ref('')
/**
* 登录前置处理(密码90天验证)
*/
async function beforeLogin() {
let validate = await beforeLoginFormRef.value.formValidation();
if (!validate) return
let formData = await beforeLoginFormRef.value.getData();
debugger
let _logonUser = formData.logonUser;
let password = formData.password;
loginStore.encodePwd = CryptoJS.AES.encrypt(password, sysConfigStore().getConfig('appKey')).toString();
let loginRes = await idaasStore.login(formData);
if (!loginRes) return
let res: any = await checkLoginUser(_logonUser)
if (res?.code != '00000') {
res?.msg && proxy.$message.error(res?.msg);
return;
}
let checkRes = res.data || {};
mobileNo.value = checkRes.mobileNo; // 当前用户的手机号
logonUser.value = checkRes.logonUser; // 当前用户的手机号
// console.log(checkRes, 'checkLoginUser');
// 继续登录逻辑
function continueLogin() {
if (import.meta.env.VITE_verify === 'false' || !checkRes.isCheckSmsValidateCode) {
handleLogin({
logonUser: logonUser.value,
password: password
})
return
}
formType.value = 'login';
nextTick(() => {
loginFormRef.value.setValue({
logonUser: logonUser.value,
mobileNo: mobileNo.value,
password: password
})
})
}
// 未到90天校验 pwdValidityDateSurplusDays密码有效期剩余天数
if (checkRes && checkRes.pwdValidityDateSurplusDays > 0) {
continueLogin()
} else {
ElMessageBox.confirm("您的密码已超过90天未修改", "提示", {
confirmButtonText: "去修改",
cancelButtonText: "继续登录",
type: "warning",
showCancelButton:false,
closeOnClickModal: false, // 禁止点击模态框背景关闭
closeOnPressEscape: false, // 禁止按下ESC键关闭
showClose: false // 可选:隐藏右上角的关闭按钮
}).then(() => {
changePassword({
mobileNo: mobileNo.value,
logonUser: logonUser.value,
name: checkRes.userName
})
}).catch(() => {
continueLogin()
})
}
}
/**
* 验证用户身份
*/
async function verifyUser() {
let validate = await loginFormRef.value.submitForm();
if (validate) {
let params = await loginFormRef.value.getData();
handleLogin(params)
}
}
/**
* 触发登录服务
* @param params
*/
async function handleLogin(params) {
loading.value = true
let client_id = params.client_id = proxy.$route.query.client_id;
let redirect_uri = params.redirect_uri = proxy.$route.query.redirect_uri;
loginStore.smsValidateCode = params.smsCode
idaasStore.login(params).then((res: any) => {
loading.value = false
const result = isJsonString(res) ? JSON.parse(res) : res;
let scope = result.data.authorities?.map(a => a.authority).join(' ');
if (!client_id) {
client_id = sysConfigStore().getConfig('appKey');
}
let hrefOrigin = window.location.origin;
if (!redirect_uri) {
redirect_uri = hrefOrigin + '/login';
}
let state = createStateHashCode();
let url = `${hrefOrigin}/idaas/oauth2/authorize?response_type=${LOGINCODE.response_type}&client_id=${client_id}&scope=other&state=${state}&redirect_uri=${encodeURIComponent(redirect_uri)}`
localStorage.setItem('idaas_code_url', url)
window.location.href = url
}).catch(() => {
loading.value = false
})
}
// !注册前的校验
// function beforeRegisterCheck(fieldName: string, errMsg?: string) {
// return {
// validator: (rule: any, value: any, callback: any) => {
// if (value && formType.value == 'register') {
// userApi.checkUser({ [fieldName]: value }).then(res => {
// res ? callback(errMsg ? new Error(errMsg) : new Error(res)) : callback()
// })
// } else {
// callback();
// }
// },
// trigger: 'blur'
// }
// }
// async function handleRegister() {
// let validate = await registerForm.value.submitForm()
// if (!validate) return
// let formData = await registerForm.value.getData(false)
// userApi.registerTenant(formData).then((res: any) => {
// if (res.data.code == proxy.$passCode) {
// proxy.$message.success('注册成功')
// registerForm.value.setValue({})
// }
// })
// }
//const logout = ref(proxy.$route.query.logout);
// // 存储跳转其他系统的url参数
// function saveQueryParams() {
// let query = proxy.$route.query;
// routeStore.fromUrl = query.fromUrl
// routeStore.toUrl = query.toUrl
// routeStore.backUrl = query.backUrl
// console.log(routeStore, 'routeStore');
// }
function setKeyUp() {
if (loginButton) {
document.onkeyup = event => {
console.log(event);
if (event.key === 'Enter') {
beforeLogin()
}
}
}
}
const passKeyRegisterloading = ref(false);
const passKeyLoginloading = ref(false);
const createCredential = async () => {
let resignForm = await beforeLoginFormRef.value.getData()
return registWebAuthn().then((res: any) => {
let options = res.data
let publicKeyCredentialCreationOptions = {
rp: {
id: options.rp.id,
name: options.rp.name
},
user: {
id: Base64url.decodeBase64url(uuidv4()),
name: resignForm.logonUser,
displayName: resignForm.logonUser,
},
challenge: Base64url.decodeBase64url(options.challenge),
pubKeyCredParams: options.pubKeyCredParams,
timeout: options.timeout,
excludeCredentials: options.excludeCredentials.map(credential => {
return {
type: credential.type,
id: Base64url.decodeBase64url(credential.id)
}
}),
authenticatorSelection: {
requireResidentKey: false,
residentKey: "preferred"
},
attestation: options.attestation,
extensions: options.extensions
};
let credentialCreationOptions = {
publicKey: publicKeyCredentialCreationOptions
};
console.log(credentialCreationOptions)
return navigator.credentials.create(credentialCreationOptions);
})
}
const passKeySign = async () => {
let validate = await beforeLoginFormRef.value.submitForm()
if (validate) {
let params = await beforeLoginFormRef.value.getData()
checkDeviceTypeRegist({
logonUser: params.logonUser,
platform: navigator.userAgentData.platform
}).then((response: any) => {
console.log(response)
if (response.data.code === '00000') {
createCredential().then((credential: any) => {
console.log(credential)
let userRQVO = {
logonUser: params.logonUser,
password: md5(params.password),
authenticator: {
clientDataJSON: Base64url.encodeBase64url(credential.response.clientDataJSON),
attestationObject: Base64url.encodeBase64url(credential.response.attestationObject),
clientExtensions: JSON.stringify(credential.getClientExtensionResults()),
deviceType: navigator.userAgentData.platform
}
}
passKeyRegisterloading.value = true;
signUp(userRQVO).then((result: any) => {
passKeyRegisterloading.value = false;
if (result?.code === '00000') {
formType.value = 'loginByPassKey'
proxy.$message.success('注册成功!');
} else {
result?.msg && proxy.$message.error(result.msg);
}
}).catch(error => {
console.log(error)
passKeyRegisterloading.value = false;
})
})
}
})
}
}
const loginWebAuthn = () => {
return getLoginWebAuthn().then((res: any) => {
let options = res.data
let publicKeyCredentialRequestOptions = {
challenge: Base64url.decodeBase64url(options.challenge),
timeout: options.timeout,
rpId: options.rpId,
allowCredentials: options.allowCredentials.map(credential => {
return {
type: credential.type,
id: Base64url.decodeBase64url(credential.id)
}
}),
userVerification: "required",
extensions: options.extensions
};
let credentialRequestOptions: any = {
publicKey: publicKeyCredentialRequestOptions
};
return navigator.credentials.get(credentialRequestOptions);
});
}
const passKeyLogin = () => {
loginWebAuthn().then((credential: any) => {
let userRQVO = {
clientDataJSON: Base64url.encodeBase64url(credential.response.clientDataJSON),
credentialId: credential.id,
authenticatorData: Base64url.encodeBase64url(credential.response.authenticatorData),
signature: Base64url.encodeBase64url(credential.response.signature),
clientExtensionsJSON: JSON.stringify(credential.getClientExtensionResults()),
}
passKeyLoginloading.value = true;
getWebAuth4jLogin(userRQVO).then(res => {
console.log(res)
passKeyLoginloading.value = false;
const result = typeof res.data == 'string' ? JSON.parse(res.data) : res.data;
if (result.code == '00000') {//第一次初始化登录。
let client_id = proxy.$route.query.client_id;
let redirect_uri = proxy.$route.query.redirect_uri;
if (!client_id) {
client_id = sysConfigStore().getConfig('appKey');
}
let hrefOrigin = window.location.origin;
if (!redirect_uri) {
//redirect_uri = import.meta.env.VITE_redirectUrl
redirect_uri = hrefOrigin + '/login';
}
let state = createStateHashCode();
let url = `${hrefOrigin}/idaas/oauth2/authorize?response_type=${LOGINCODE.response_type}&client_id=${client_id}&scope=other&state=${state}&redirect_uri=${encodeURIComponent(redirect_uri)}`
localStorage.setItem('idaas_code_url', url)
window.location.href = url
}
}).catch(error=>{
console.log(error)
passKeyLoginloading.value = false;
})
});
}
/**
* 直接去获取验证码表单
*/
function toCheckSmsCode() {
let isCheckSms = loginStore.isCheckSms;
if (isCheckSms) {
let password = CryptoJS.AES.decrypt(loginStore.encodePwd || '', sysConfigStore().getConfig('appKey')).toString(CryptoJS.enc.Utf8)
let principal = idaasStore.idaasUserInfo.principal
formType.value = 'login';
nextTick(()=>{
loginFormRef.value.setValue({
logonUser: principal?.logonUser,
mobileNo: principal?.mobileNo,
password: password
})
loginStore.isCheckSms = false // 重置状态
})
}
}
onBeforeMount(() => {
// 子系统退出不需要清除门户的token
// if (logout.value && logout.value == '1') {
// userStore.logout({
// logMessage:'logout-正常触发退出登录'
// });
// }
/// saveQueryParams();
toCheckSmsCode();
});
onMounted(()=>{
// new DevicePixelRatio().init();
setKeyUp()
})
onBeforeUnmount(()=>{
document.onkeyup = null
})
</script>
<style lang="scss" scoped>
.main-h {
height: 100%;
}
.login_form {
position: relative;
background: #FFFFFF;
box-shadow: 0px 4px 10px 0px rgba(0, 0, 0, 0.2);
border-radius: 2px;
}
.login-size {
padding: 60px 50px;
width: 440px;
height: 430px;
}
.register-size {
padding: 40px 32px;
width: 548px;
height: 580px;
}
.login-footer {
display: flex;
flex-direction: column;
}
:deep(.el-button--primary) {
background-image: linear-gradient(116deg, #0C48F5 0%, #23D6D1 95%);
&.is-disabled {
background-image: none;
}
}
.login-img {
cursor: pointer;
width: 72px;
height: 72px;
position: absolute;
top: 0;
right: 0;
}
.login-title {
font-family: PingFangSC-Semibold;
font-size: 24px;
color: #212121;
letter-spacing: 0;
line-height: 36px;
font-weight: 600;
}
:deep(.xieyi) {
margin-bottom: 10px;
.el-checkbox__input.is-checked + .el-checkbox__label {
color: #666;
}
}
.bg-banner {
// position: fixed;
// z-index: 1001;
width: 100%;
height: 90%;
background-image: url('../../assets/images/login-bg.png');
background-size: cover;
/* 背景图覆盖整个元素 */
background-position: center;
/* 背景图居中 */
background-repeat: no-repeat;
/* 防止背景图重复 */
}
#login-box {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
// position: absolute;
// top: 50%;
// left: 50%;
// transform: translateX(-50%) translateY(-51%);
// background-color: #fff;
.login-form {
display: flex;
flex-direction: column;
justify-content: center;
min-height: 500px;
width: 368px;
overflow: hidden;
.title-container {
position: relative;
.title {
font-size: 28px;
color: #212121;
letter-spacing: 0;
text-align: center;
line-height: 42px;
font-weight: 600;
margin-bottom: 32px;
}
}
}
:deep(.el-input) {
height: 36px;
}
}
.copyright_text {
width: 100%;
font-size: 12px;
color: #2c3e50;
line-height: 18px;
text-align: center;
font-weight: 400;
position: absolute;
bottom: 2px;
left: 50%;
transform: translateX(-50%);
}
.forget-pwd-btn {
cursor: pointer;
margin-top: 12px;
font-size: 14px;
color: #999999;
line-height: 21px;
font-weight: 400;
}
.code-desc {
font-size: 12px;
color: #999999;
letter-spacing: 0;
line-height: 17px;
font-weight: 400;
margin-top: 8px;
}
.pb-22px {
padding-bottom: 22px;
}
.overflow-auto {
overflow: auto;
}
.pt-30px {
padding-top: 30px;
}
.flex {
display: flex;
}
.loginButton.is-disabled {
background: rgb(159.5, 206.5, 255);
color: #fff;
}
.el-button.is-link:hover {
color: inherit;
}
</style>
<!--登录页-->
<!--登录页-->
<style scoped>
.main {
height: 100%;
width: 100%;
overflow-y: auto;
}
.content {
background: #fff;
line-height: 34px;
width: 1300px;
text-align: left;
margin: 20px auto;
}
h1 {
font-size: 20px;
text-align: center;
font-weight: bold;
}
h2 {
font-size: 16px;
font-weight: bold;
margin-top: 8px;
margin-bottom: 8px;
}
h3,
p {
text-indent: 2em;
margin-top: 8px;
margin-bottom: 8px;
}
h4 {
text-indent: 2em;
font-weight: normal;
margin: 5px 0;
}
</style>
<template>
<div class="main">
<div class="content">
<h1>用户服务协议</h1>
<h2>第一条 重要提示</h2>
<p>
欢迎申请并使用数据资产运营平台(以下简称“平台”)所提供的各项服务。请您(以下亦称“用户”)在登录平台前,仔细阅读本协议之全部条款,并确认您已完全理解本协议之规定,包括但不限于知识产权条款、法律适用及争议解决条款等涉及您重大权益及义务的条款,如您对本协议有任何疑问,请立即停止登录或使用平台,并可向北京传世博润科技有限公司客服咨询。
</p>
<h2>第二条 协议的接受与变更</h2>
<p>
2.1
在您充分阅读并且理解本协议,并勾选本协议前选项框或点击登录按钮成功登录,您的行为表示您同意并签署了本协议,表明您确定自己具有接受平台服务的权利和行为能力,并自愿接受本协议全部条款的约束,本协议构成您与北京传世博润科技有限公司(以下简称“我们”)及其经营的平台之间具有约束力的法律文件。
</p>
<p>
2.2
我们会根据国家法律法规变化及运营需要不时的对本协议进行修改,修改后的协议一经公布既有效的代替原协议。用户有义务不时关注并阅读最新版本的协议、声明、规则等内容。如果您不同意本协议的修改,请立即停止访问或使用本软件或取消已经获得的服务;如果您选择在本协议修改并公布后继续访问或使用,则视为您同意接受本协议的修改。
</p>
<p>
2.3
您与我们签署的本协议所列明的条款,并不能完全涵盖您与我们所有的权利和义务。因此,我们公布的其他声明、规则等均视为本协议之补充协议,为本协议不可分割的组成部分,与本协议具有同等的法律效力。若其他声明、规则等与服务协议存在任何冲突或不一致的,以本协议为准。
</p>
<h2>第三条 账户的取得与使用</h2>
<p>
3.1
您需确认,在您开始使用平台前,应当具有中华人民共和国法律规定的与您行为相适应的民事行为能力。若您不具备前述与您行为相适应的民事行为能力,则您及您的监护人应按照法律的规定承担因此而导致的一切后果。
</p>
<p>3.2
您有权选择是否成为平台用户。</p>
<p>
3.3
您选择成为平台用户时,应按照平台客服的提示及要求填写或提供资料、信息,并确保资料、信息的真实性、正确性及完整性。如果您的资料、信息发生变化,应及时修改。
</p>
<p>
3.4
成为平台用户后将得到一个用户名和密码,用户凭用户名和密码享受平台向其会员用户提供的服务。
</p>
<p>
3.5
用户将对用户名和密码安全负全部责任,并且用户对以其用户名进行的所有活动和事件负全责。用户有权根据平台规定的程序修改密码。非经平台书面同意,用户名和密码不得擅自转让或者授权他人使用,否则用户应承担由此造成的一切后果。
</p>
<p>
3.6
用户若发现任何非法使用用户账号或存在安全漏洞的情况,请立即通告本公司。</p>
<p>
3.7
为改善用户体验、完善服务内容,我们将不断努力开发新的产品和服务,并为您不时提供软件更新(这些更新可能会采取软件替换、修改、功能强化、版本升级等形式)。为保证本软件安全性和功能的一致性,我们有权不经向您特别通知而对软件进行更新,或者对软件的部分功能效果进行改变或限制。本软件不保证旧版本软件继续可用及相应的客户服务,请您随时核对并下载最新版本。
</p>
<h2>第四条 您的权利和义务</h2>
<p>4.1 用户有权利拥有自己在平台的用户名和密码并有权利使用自己的用户名和密码随时登录平台。</p>
<p>
4.2 用户有权利享受平台提供的互联网技术和信息服务,并有权利在接受平台提供的服务时获得平台的技术支持、咨询等服务。
</p>
<p>4.3 用户保证不会利用技术或其他手段破坏及扰乱平台。</p>
<p>
4.4 用户应尊重平台及其他第三方的知识产权和其他合法权利,平台保留用户侵犯平台知识产权时终止向该用户提供服务并不退还任何款项的权利。
</p>
<p>
4.5 对由于用户给平台提供的联络方式有误而导致的一切后果,用户应自行承担责任,包括但不限于因用户未能及时收到平台的相关通知而导致的后果和损失。
</p>
<p>
4.6
用户保证其使用平台服务时将遵从国家、地方法律法规、行业惯例和社会公共道德,不会利用平台提供的服务进行存储、发布、传播如下信息和内容:违反国家法律法规政策的任何内容(信息);违反国家规定的政治宣传和/或新闻信息;涉及国家秘密和/或安全的信息;封建迷信和/或淫秽、色情、下流的信息或教唆犯罪的信息;博彩有奖、赌博游戏;违反国家民族和宗教政策的信息;妨碍互联网运行安全的信息;侵害他人合法权益的信息和/或其他有损于社会秩序、社会治安、公共道德的信息或内容。用户同时承诺不得为他人发布上述不符合国家规定和本服务条款约定的信息内容提供任何便利,包括但不限于设置URL、BANNER链接等。用户承认平台有权在用户违反上述时有权终止向用户提供服务并不与退还任何款项,因用户上述行为给平台造成损失的,用户应予赔偿。
</p>
<h2>第五条 平台的权利和义务</h2>
<p>5.1 平台应根据用户类型向用户提供合格的网络技术和信息服务。</p>
<p>5.2 平台承诺对用户资料采取对外保密措施,不向第三方披露用户资料,不授权第三方使用用户资料,除非:</p>
<h4>(1)依据本协议条款或者用户与 平台之间其他服务协议、合同、在线条款等规定可以提供;</h4>
<h4>(2)依据法律法规的规定应当提供;</h4>
<h4>(3)行政、司法等有权部门要求本公司提供;</h4>
<h4>(4)用户同意平台向第三方提供;</h4>
<h4>(5)平台解决举报事件、提起诉讼而提交的;</h4>
<h4>(6)平台为防止严重违法行为或涉嫌犯罪行为发生而采取必要合理行动所必须提交的;</h4>
<h4>
(7)平台为向用户提供产品、服务、信息而向第三方提供的,包括平台通过第三方的技术及服务向用户提供产品、服务、信息的情况。
</h4>
<p>5.3 平台有权使用用户资料。</p>
<p>
5.4 平台有权利对用户进行审查并决定是否接受用户成为平台会员或是否与用户进行某一交易。
</p>
<p>
5.5
平台保留在用户违反国家、地方法律法规规定或违反本注册条款的情况下终止为用户提供服务并终止用户账号的权利,并且在任何情况下,平台对任何间接、偶然、特殊及继起的损害不负责任。
</p>
<h2>第六条 免责说明</h2>
<p>
6.1 我们作为网络服务的提供者,不保证平台系统能充分满足您的需求,您在接受平台服务的过程中,可能遇到的错误、不作为等事件,我们不承担法律责任。
</p>
<p>
6.2
您作为平台系统的用户有义务确保在平台系统中录入数据的真实性,我们作为平台应用的提供者,不对您在平台系统中录入数据的真实性承担法律责任。
</p>
<p>
6.3
基于互联网的特殊性,我们也不能保证您在使用我们提供的服务过程中,服务不中断,我们对服务的及时性、安全性不作保证,不承担非因我们导致的任何责任。我们竭力保证您对本网站进行安全的访问和使用,但我们不保证网站或服务器不含病毒或其他潜在有害因素。
</p>
<p>
6.4
如果我们发现或收到他人举报或投诉您在使用过程中违反任何法律法规、公序良俗或本协议的规定,我们有权要求不经通知随时对账号进行处理,并视情节对违规账号处以包括但不限于警告、限制或禁止使用部分或全部产品及服务,账号封禁直至注销。
</p>
<p>
6.5 您应从本公司官网或其他拥有平台授权的第三方获得安装程序。如果您从未经平台授权的第三方获得平台安装程序或名称相同的安装程序,我们及平台无法保证软件能够正常使用,并对因此给您造成损失不予负责。
</p>
<p>
6.6
您理解并同意,因违法法律法规、公序良俗或本协议的规定,导致或产生第三方主张的任何索赔、要求或损失,导致或产生第三方主张的任何索赔、要求或损失,您应当独立承担全部责任。我们或平台因此遭受损失的,您也应当一并赔偿。
</p>
<h2>第七条 知识产权归属与授权</h2>
<p>
平台的一切商标权、专利权等知识产权和商业秘密,以及与平台相关的所有信息内容。包括但不限于图标、图片、色彩、界面设计、版面框架、有关数据等均受中华人民共和国著作权法、商标法、专利发、反不正当竞争法和相应的国际条约以及其他知识产权法律法规的保护,除涉及第三方授权的知识产权外,平台享有上述所有知识产权,未经平台书面同意,用户不得为任何营利或非营利性的目的自行实施、利用转让或许可任何第三方实施、利用、转让上述知识产权,平台保留追究上述未经许可的法律责任的权利。您对所使用的软件有其专属性使用权,但不得自行或许可任何第三方复制、修改、出售或衍生产品。
</p>
<h2>第八条 服务的终止</h2>
<p>8.1 用户有权随时申请终止其会员资格。</p>
<p>
8.2 平台有权根据实际情况决定取消为用户提供服务,视情况确定是否退还用户为该服务所交纳款项的剩余部分,本公司不承担其他任何责任。
</p>
<h2>第九条 法律适用、管辖和其他</h2>
<p>
9.1
本协议的生效、履行、解释及争议解决均适用中华人民共和国法律。您因使用平台而产生或与之相关的一切争议,权利主张或其他思想,均适用中华人民共和国法律。
</p>
<p>
9.2 您与我们及我们经营的平台发生的一切争议,应友好协商,如协商不成的,应提交北京市朝阳区人民法院管辖。
</p>
<p>
9.3
我们可能根据运营需要,不时的发布针对包含您在内的用户的相关协议,并可能将该相关协议作为对本协议的补充或修改,使其成为本协议的一部分。请您及时关注并阅读相关协议。
</p>
<p>9.4 本协议部分条款被视为废止、无效或不可执行,本协议其余条款仍应有效并具有约束力与可执行性。</p>
<p>9.5 我们未行使或执行本服务协议任何权利或规定,不构成对前述权利或权益之放弃。</p>
<p>
【审慎提醒】如您点击“同意”、勾选相关接受提示。则本协议立即生效,并构成您和北京传世博润科技有限公司及其经营的平台之间有约束力的法律文件。
</p>
</div>
</div>
</template>
<script>
export default {};
</script>
<!--登录页-->
<!--登录页-->
<style scoped>
.main {
height: 100%;
width: 100%;
overflow-y: auto;
}
.content {
background: #fff;
line-height: 34px;
width: 1300px;
text-align: left;
margin: 20px auto;
}
h1 {
font-size: 20px;
text-align: center;
font-weight: bold;
}
h2 {
font-size: 16px;
font-weight: bold;
margin-top: 8px;
margin-bottom: 8px;
}
h3,
p {
text-indent: 2em;
margin-top: 8px;
margin-bottom: 8px;
}
h4 {
text-indent: 2em;
font-weight: normal;
margin: 5px 0;
}
</style>
<template>
<div class="main">
<div class="content">
<h1>数据资产运营平台隐私政策</h1>
<p>
【特别提示】数据资产运营平台非常重视用户的隐私和个人信息保护。您在使用数据资产运营平台的产品与服务时,我们可能会收集和使用您的相关信息。我们希望通过本隐私政策向您说明我们在您使用我们的产品、服务时如何收集、使用、保存、共享和转让这些信息,以及我们为您提供的访问、更新、删除和保护这些信息的方式。
</p>
<p>
请您在使用数据资产运营平台的产品及服务前,仔细阅读并充分理解本隐私政策,重点内容我们已经用粗体标识,请您特别关注。在阅读过程中,如果您不同意相关政策或其中任何条款约定,您应立即停止使用平台的产品及服务。您在点击“同意”按钮后,本隐私政策即构成对双方有约束力的法律文件,即表示您同意我们按照本隐私政策收集、使用、处理和存储您的相关信息。如果您对本隐私政策有任何疑问、意见或建议,可通过本隐私政策提供的联系方式与我们联系。
</p>
<p style="font-weight: 700;">该平台依据TC609-6-2025-01《可信数据空间 技术架构》、TC609-6-2025-14《可信数据空间
数字合约技术要求》、TC609-6-2025-15《可信数据空间 使用控制技术要求》、《数据基础设施 参考架构》、《数据基础设施 互联互通基本要求》、《数据基础设施 用户身份管理和接入要求》、《数据基础设施
标识要求》、《数据基础设施 接入连接器技术要求》、《数据基础设施 数据目录描述要求》相关标准要求进行开发。</p>
<h2>一、适用范围</h2>
<p>
1、本隐私政策适用于平台的运营主体,具体是指北京传世博润科技有限公司(以下称“我们”)。前述数据资产运营平台产品和服务的使用人在本隐私政策中称为“您”。
</p>
<p>
2、我们深知个人信息对您的重要性,我们将按法律法规要求,采取相应安全保护措施,保护您的个人信息安全。
</p>
<h2>二、隐私政策的主要内容</h2>
<p>
1、我们如何收集和使用您的信息
</p>
<p>2、Cookie和类似技术的使用</p>
<p>
3、我们如何共享、转让、公开披露您的信息
</p>
<p>
4、我们如何存储和保护您的信息
</p>
<p>
5、您如何管理您的信息
</p>
<p>
6、本隐私政策的适用及更新</p>
<p>
7、如何联系我们
</p>
<h2>三、隐私政策的具体内容</h2>
<h3>1、我们如何收集和使用您的信息</h3>
<p>
个人信息,是指以电子或者其他方式记录的能够单独或者与其他信息结合识别特定自然人身份或者反映特定自然人活动情况的各种信息。本隐私政策中涉及的个人信息包括:个人基本资料(个人姓名、个人电话号码);个人身份信息(身份证信息);个人生物识别信息(面部特征);网络身份标识信息(个人信息主体账号、IP地址、个人数字证书及与前述有关的密码、口令、口令保护答案);个人通讯信息(通讯录);个人常用设备信息(硬件型号、设备MAC地址、操作系统类型、软件列表唯一设备识别码(android
ID/IDFA));</p>
<p>
个人敏感信息,是指一旦泄露、非法提供或滥用可能危害人身和财产安全,极易导致个人名誉、身心健康受到损害或歧视性待遇的个人信息。本隐私政策中涉及的个人敏感信息包括:个人身份信息(身份证);网络身份标识信息(个人信息主体账号、IP地址、个人数字证书及与前述有关的密码、口令、口令保护答案);个人通讯信息(通讯录);个人上网记录(软件使用记录、点击记录、收藏列表);其他信息等。
</p>
<p>为了向您提供更好的服务,我们根据您所使用服务的不同,收集您与服务相关的不同信息。</p>
<p style="font-weight: 700;">
特别提示您注意,如信息无法单独或结合其他信息识别到您的个人身份,其不属于法律意义上您的个人信息;当您的信息可以单独或结合其他信息识别到您的个人身份时或我们将无法与任何特定个人信息建立联系的数据与其他您的个人信息结合使用时,这些信息在结合使用期间,将作为您的个人信息按照本隐私政策处理与保护。
</p>
<p>(一)依据法律法规及监管规定履行法定义务</p>
<p>
针对各项服务,我们需要按照法律法规及监管规定履行相应的法定义务,包括资质审核、采取风险防范措施。当您需要使用数据资产运营平台服务时,为了遵守法律法规对交易资质的要求,对您的身份进行识别,数据资产运营平台可能会收集您的身份信息、联系方式及认证信息。同时,为了验证您提供信息的准确性和完整性,我们可能会与合法留存您的信息的国家机关、金融机构、企事业单位进行核对。如果您不提供前述信息,将无法使用与监管要求相关的部分服务,但不影响您使用我们提供的其他服务。
</p>
<p>(二)为您提供产品或服务</p>
<p>1)注册、登陆</p>
<p>当您在平台创建账户进行注册时,您必须向我们提供您的基本信息(手机号码),并设置登录密码。如果您不提供前述信息,则您无法注册/登录平台账户,且无法使用需注册/登录的功能,但不会影响您正常使用无需注册/登录的功能。</p>
<p>2)浏览</p>
<p>您可在平台上浏览轮播图、资讯信息、产品介绍等。为此,我们可能会收集您使用数据资产运营平台时的设备信息,包括<span
style="font-weight: 700;">设备名称、设备型号、唯一设备标识符、操作系统、应用程序版本、内存信息、CPU信息、硬盘序列号、登录IP地址、登录MAC地址、设备分辨率、内网IP地址、公网IP地址、端口号、CPU序列号、浏览器类型</span>以此来为您提供信息展示的最优方案。此外,在您使用浏览功能的过程中,我们会自动收集您使用数据资产运营平台的详细情况,并作为有关的网络日志保存,包括您输入的搜索关键词信息和点击的链接,您浏览的内容及评论信息,访问的日期和时间,以及您请求的网页记录、操作系统、软件版本号、IP信息。如果您不提供前述信息,则您无法使用浏览功能。
</p>
<p>3)搜索功能</p>
<p>
当您使用数据资产运营平台提供的搜索功能时,我们会收集您查询的关键字信息以及您在使用数据资产运营平台服务时所浏览的或要求的其他信息和内容详情。为了给您带来更便捷的搜索服务并不断完善数据资产运营平台的产品和服务,我们可能会使用浏览器网络存储机制和应用数据缓存,您搜索的信息我们可能进行本地存储。该等关键词信息通常无法单独识别您的个人身份,不在本隐私政策的限制范围内。只有当您的搜索关键词信息与您的其他信息有联结并可识别您的个人身份时,则在结合使用期间,我们会将您的搜索关键词信息作为您的个人信息,与您的搜索历史记录一同按照本隐私政策对其进行处理与保护。
</p>
<p>(三)安全运行</p>
<p>1)为了保障软件与服务的安全运行,我们会收集您的软件版本号服务日志、手机状态信息。</p>
<p>2)如果您不提供上述信息,您将无法享受我们提供的产品与/或服务。</p>
<p>(四)您可选择是否授权我们收集和使用您的个人信息的情形</p>
<p>为提升您的服务体验及改进服务质量,或者为您推荐更优质或更适合的服务,数据资产运营平台会向您申请下列与个人信息相关的系统权限:</p>
<p>1)基于位置信息的个性化推荐服务:我们会在您开启位置权限后访问获取您的位置信息,根据您的位置信息为您提供更契合您需求的页面展示、产品或服务;</p>
<p>2)基于相机/摄像头的附加服务:您可在开启相机/摄像头权限后使用该功能进行扫码用于拍摄照片用于上传系统管理功能的需要,包含且不限于如身份验证、资料拍摄等。</p>
<p>
3)基于详细文件的访问、上传及存储的附加服务:您可在开启对应文件的权限后使用该功能上传您的照片/图片,以实现上传系统管理功能的需要,包含且不限于如企业认证、经办人授权、连接器认证、数据产品管理、数据产品上架等的功能;我们可能会存储您所上传的文件来符合系统管理功能的需要。
</p>
<p>4)基于麦克风的语音技术相关附加服务:您可在开启麦克风权限后使用麦克风实现替代手工输出文字的操作。请您知晓,即使您已同意开启麦克风权限,我们也仅会在您主动点击客户端内麦克风图标通过麦克风获取语音信息。</p>
<p>
您理解并同意,上述附加服务可能需要您在您的设备中开启您的位置信息(地理位置)、摄像头(相机)、文件(图片库/存储)、麦克风(语音)的访问权限,以实现这些权限所涉及信息的收集和使用。您可在您的设备“设置”中逐项查看上述权限的状态,并可自行决定这些权限随时的开启或关闭。请您注意,您开启任一权限即代表您授权我们可以收集和使用相关个人信息来为您提供对应服务,您一旦关闭任一权限即代表您取消了授权,我们将不再基于对应权限继续收集和使用相关个人信息,也无法为您提供该权限所对应的服务。但是,您关闭权限的决定不会影响此前基于您的授权所进行的信息收集及使用。
</p>
<p>(五)其他用途</p>
<p>若您提供的信息中含有第三方的个人信息,在向数据资产运营平台提供这些个人信息之前,您需确保您已经取得合法的授权。</p>
<p>(六)根据相关法律法规及国家标准,在以下情形中,我们可能会依法收集并使用您的个人信息无需征得您的同意:</p>
<p>1)与国家安全、国防安全直接相关的;</p>
<p>2)与公共安全、公共卫生、重大公共利益直接相关的;</p>
<p>3)与刑事侦查、起诉、审判和判决执行直接相关的;</p>
<p>4)出于维护您或他人的生命、财产重大合法权益但又很难得到您本人同意的;</p>
<p>5)所收集的个人信息是您自行向社会公众公开的;</p>
<p>6)从合法公开披露的信息中收集的个人信息,包括合法的新闻报道、政府信息公开渠道;</p>
<p>7)根据您的要求签订和履行合同所必需的;</p>
<p>8)用于维护所提供的服务的安全稳定运行所必需的,包括发现、处置产品或服务的故障;</p>
<p>9)与我们履行法律法规规定的义务相关的;</p>
<p>10)法律法规规定的其他情形。</p>
<h3>2、 Cookie和类似技术的使用</h3>
<p style="text-indent:2em;">
为确保网站正常运转,我们会在您的计算机或移动设备上存储名为Cookie的小数据文件。Cookie通常包含标识符、站点名称以及一些号码和字符。借助于Cookie,网站能够存储您的登录状态的数据,提升服务/产品质量及优化用户体验。我们不会将
Cookie用于本隐私政策所述目的之外的任何用途。
</p>
<h3>3、我们如何共享、转让、公开披露您的信息</h3>
<p>(一)共享</p>
<p style="text-indent:2em;">
您的个人信息是我们为您提供服务的重要部分,我们会遵循法律规定对您的信息承担保密义务。除以下情形外,我们不会将您的信息披露给第三方:
</p>
<p style="text-indent:2em;">
1)法定情形下的共享:我们可能会根据法律法规规定、诉讼、争议解决需要,或按行政、司法机关依法提出的要求,对外共享您的个人信息。
</p>
<p style="text-indent:2em;">
2)在获取明确同意的情况下共享:获得您的明确同意后,我们会与其他方共享您的个人信息。
</p>
<p style="text-indent:2em;">
3)在您主动选择情况下共享:您通过数据资产运营平台使用的必要信息会共享给您劳动关系所属企业或您使用数据资产运营平台所服务的企业或您授权可共享的企业。
</p>
<p style="text-indent:2em;">
4)为您提供服务的目的,您的信息可能会与我们的关联公司共享,我们只会共享必要的个人信息,且受本隐私政策约束。如果关联公司超出本隐私政策使用及处理您的信息,将再次征求您的授权同意。
</p>
<p>(二)转让</p>
<p style="text-indent:2em;">我们不会将您的个人信息转让给任何公司、组织和个人,但以下情况除外:</p>
<p style="text-indent:2em;">1)事先获得您的明确同意;</p>
<p style="text-indent:2em;">2)根据法律法规或强制性的行政或司法要求;</p>
<p style="text-indent:2em;">
3)转让经去标识化处理的个人信息,且确保数据接收方无法重新识别或者关联个人信息主体;
</p>
<p style="text-indent:2em;">
4)在涉及资产转让、收购、兼并、重组或破产时,如果涉及到个人信息转让,我们会向您告知有关情况,并要求新的持有您个人信息的公司、组织继续受本隐私政策的约束。如果变更个人信息使用目的时,我们将要求该公司、组织重新取得您的明示同意。如果破产且无承接方的,我们将对数据做删除处理。
</p>
<p>(三)公开披露 我们仅会在以下情况下,公开披露您的个人信息:</p>
<p style="text-indent:2em;">1)获得您明确同意或基于您的主动选择,我们可能会公开披露您的个人信息;</p>
<p style="text-indent:2em;">
2)如果我们确定您出现违反法律法规或严重违反数据资产运营平台云平台相关协议及规则的情况,或为保护其他用户或公众的人身财产安全免遭侵害,我们可能依据法律法规或数据资产运营平台的相关协议规则披露关于您的个人信息,包括相关违规行为以及数据资产运营平台已对您采取的措施。
</p>
<p>(四)共享、转让、公开披露个人信息时事先征得授权同意的例外</p>
<p style="text-indent:2em;">
以下情形中,共享、转让、公开披露您的个人信息无需事先征得您的授权同意:
</p>
<p style="text-indent:2em;">1)与国家安全、国防安全有关的;</p>
<p style="text-indent:2em;">2)与公共安全、公共卫生、重大公共利益有关的;</p>
<p style="text-indent:2em;">3)与犯罪侦查、起诉、审判和判决执行司法或行政执法有关的;</p>
<p style="text-indent:2em;">
4)出于维护您或其他个人的生命、财产重大合法权益但又很难得到您本人同意的;
</p>
<p style="text-indent:2em;">5)所收集的个人信息是您自行向社会公众公开的;</p>
<p style="text-indent:2em;">
6)从合法公开披露的信息中收集的个人信息,包括合法的新闻报道、政府信息公开渠道。
</p>
<p style="text-indent:2em;">
请知悉,根据适用的法律,若我们对个人信息采取技术措施和其他必要措施进行处理,使得数据接收方无法重新识别特定个人且不能复原,则此类处理后数据的共享、转让、公开披露无需另行向您通知并征得您的同意。
</p>
<h3>4、我们如何存储和保护您的信息</h3>
<p>(一)技术措施与数据保护措施</p>
<p style="text-indent:2em;">
1)我们在保护您的个人信息上将不断努力,争取采取符合业界标准、合理可行的安全防护措施保护您提供的个人信息安全,防止个人信息遭到未经授权访问、公开披露、使用、修改、损坏或丢失。在您的浏览器与服务器之间交换数据时受SSL(Secure Socket Layer)协议加密保护;我们会使用加密技术提高个人信息的安全性;我们会使用受信赖的保护机制防止个人信息遭到恶意攻击;我们会部署访问控制机制,与相关处理数据的员工签署保密协议,尽力确保只有授权人员才可访问个人信息;以及我们会举办安全和隐私保护培训课程,加强员工对于保护个人信息重要性的认识。另外,我们还会聘请外部独立第三方对我们的信息安全管理体系进行评估。
</p>
<p style="text-indent:2em;">
2)我们有行业先进的以数据为核心,围绕数据生命周期进行的数据安全管理体系,从组织建设、制度设计、人员管理、产品技术各方面多维度提升整个系统的安全性。<span style="font-weight: 700;">目前,数据资产运营平台已经通过了国家信息安全等级保护(三级)的测评和备案。</span>
</p>
<p style="text-indent:2em;">
3)互联网并非绝对安全的环境,我们强烈建议您不要使用非数据资产运营平台推荐的通信方式发送个人信息。在使用数据资产运营平台服务进行网上交易时,您不可避免地要向交易对方或潜在的交易对方披露自己的个人信息,包括联络方式或联系地址。请您妥善保护自己的个人信息,仅在必要的情形下向他人提供。如果您发现自己的信息发生泄露,请您立即联络我们,以便我们根据您的申请采取相应措施。请注意,您在使用我们服务时自愿共享甚至公开分享的信息,可能会涉及您或他人的个人信息甚至个人敏感信息。请您更加谨慎地考虑,是否在使用我们的服务时共享甚至公开分享相关信息。
</p>
<p>4)请使用复杂密码,协助我们保证您的账号安全。</p>
<p>(二)存储期限</p>
<p style="text-indent:2em;">
1)我们会采取合理可行的措施,尽力避免收集无关的个人信息。我们只会在达成本隐私政策所述目的所需的期限内保存您的个人信息。数据存储期限为二十年,除非法律有强制的存留要求。如果我们更改数据存储时间,会另行通知您。在您的个人信息超出保留期间后或您要求注销账户后,我们将删除您的个人信息或匿名化处理,法律法规另有规定的除外。
</p>
<p>
(三)我们在中华人民共和国境内运营收集和产生的个人信息,存储在中国境内,以下情形除外:
</p>
<p style="text-indent:2em;">1)法律法规有明确规定;</p>
<p style="text-indent:2em;">2)获得您的明确授权。</p>
<p>(四)安全事件处理</p>
<p style="text-indent:2em;">
在不幸发生个人信息安全事件后,我们将按照法律法规的要求向您告知:安全事件的基本情况和可能的影响、我们已采取或将要采取的处置措施、您可自主防范和降低风险的建议、对您的补救措施。事件相关情况我们将以邮件、信函、电话、推送通知的方式告知您,难以逐一告知个人信息主体时,我们会采取合理、有效的方式发布公告。同时,我们还将按照监管部门要求,上报个人信息安全事件的处置情况。
</p>
<p style="font-weight: 700;">尽管已经采取了上述合理有效措施,并已经遵守了相关法律规定要求的标准,但请您理解,由于技术的限制以及可能存在的各种恶意手段,在互联网行业,即便竭尽所能加强安全措施,也不可能始终保证信息百分之百的安全,我们将尽力确保您提供给我们的个人信息的安全性。</p>
<p style="font-weight: 700;">您知悉并理解,接入我们的服务所用的系统和通讯网络,有可能因我们可控范围外的因素而出现问题。因此,我们强烈建议您采取积极措施保护个人信息的安全,请根据平台要求必须使用复杂密码以及定期修改密码机制、不将自己的账号密码及相关个人信息透露给他人。</p>
<h3>5、 您如何管理您的信息</h3>
<p>(一)查询、更正或补充您的个人信息</p>
<p>
您有权查询、更正或补充您的信息,您可以通过登录数据资产运营平台,进入“用户信息”的方式自行进行查询、更正或补充。
</p>
<p>(二)删除您的个人信息</p>
<p style="text-indent:2em;">
1)一般而言,我们只会在法律法规定或必需且最短的时间内保存您的个人信息。
</p>
<p style="text-indent:2em;">
2)对于您的部分个人信息,我们在产品的相关功能页面为您提供了操作指引和操作设置,您可以直接进行删除。
</p>
<p style="text-indent:2em;">
3)在以下情形中,您可以登录数据资产运营平台,进入“用户信息-注销登录”,自主发起删除个人信息的请求,或联系数据资产运营平台客服或工作人员,向我们提出删除个人信息的请求,但已做数据匿名化处理或法律法规另有规定的除外:
</p>
<p style="text-indent:2em;">①如果我们处理个人信息的行为违反法律法规;</p>
<p style="text-indent:2em;">②如果我们收集、使用您的个人信息,却未征得您的同意;</p>
<p style="text-indent:2em;">③如果我们处理个人信息的行为违反了与您的约定;</p>
<p style="text-indent:2em;">④如果您不再使用我们的产品或服务,或您注销了账号。</p>
<p style="text-indent:2em;">
4)您理解并同意:当您或我们协助您删除相关信息后,因为适用的法律和安全技术,我们可能无法立即从备份系统中删除相应的信息,我们将安全地存储您的个人信息并将其与任何进一步处理隔离,直到备份可以清除或实现匿名。
</p>
<p>(三)账户注销</p>
<p style="text-indent:2em;">
您可以通过在数据资产运营平台进行自行注销或联系数据资产运营平台的客服/工作人员进行账户注销。当您符合约定的账户注销条件并注销某项服务相关账户后,您该账户内的所有信息将被清空,我们将不会再收集、使用或对外提供与该账户相关的个人信息,但我们仍需按照监管要求的时间保存您在使用服务期间提供或产生的信息,且在该依法保存的时间内有权机关仍有权依法查询。
</p>
<p>(四)获取您的个人信息副本</p>
<p style="text-indent:2em;">
您可以联系数据资产运营平台客服申请获取您已经提供给数据资产运营平台的个人信息副本(包括:基本资料、身份信息)。
</p>
<p>(五)约束信息系统自动决策</p>
<p style="text-indent:2em;">
在某些业务功能中,我们可能仅依据信息系统、算法等在内的非人工自动决策机制做出决定。如果这些决定显著影响您的合法权益,您有权联系数据资产运营平台客服要求我们做出解释或要求提供适当的救济方式。
</p>
<p>(六)响应您的请求</p>
<p style="text-indent:2em;">
如果您无法自主查询、更正或删除您的个人信息,或者其中部分功能系统不支持的,您均可以通过本隐私政策下方的联系方式与我们联系。为了保障安全,我们可能需要您提供书面请求或以其他方式标明您的身份,我们将在收到您的反馈并在验证您的身份后15天内答复您的请求。
</p>
<p>
(七)如果您发现我们收集、使用您个人信息的行为,违反了法律法规规定或违反了与您的约定,您可联系相应的电话或通过客服,要求删除相应的信息。
</p>
<p>
(八)尽管有上述约定,但按照相关法律法规及国家标准,在以下情形中,我们可能无法响应您的请求:
</p>
<p style="text-indent:2em;">1)与国家安全、国防安全直接相关的;</p>
<p style="text-indent:2em;">2)与公共安全、公共卫生、重大公共利益直接相关的;</p>
<p style="text-indent:2em;">3)与刑事侦查、起诉、审判和执行判决直接相关的;</p>
<p style="text-indent:2em;">4)有充分证据表明您存在主观恶意或滥用权利的;</p>
<p style="text-indent:2em;">5)响应您的请求将导致其他个人、组织的合法权益受到严重损害的;。</p>
<p style="text-indent:2em;">
6)出于维护您或其他个人的生命、财产重大合法权益但又很难得到您本人授权同意的;。
</p>
<p style="text-indent:2em;">7)涉及商业秘密的;</p>
<p style="text-indent:2em;">8)与我们履行法律法规规定的义务相关的。</p>
<h3>6、  本隐私政策的适用及更新。</h3>
<p>
(一)数据资产运营平台所有服务均适用本隐私政策,除非相关服务已有独立的隐私权政策。
</p>
<p>
(二)必要情况下,我们可能会对《数据资产运营平台隐私政策》进行修订。请您注意,只有在您确认修订后的《数据资产运营平台隐私政策》后,我们才会按照修订后的政策收集、使用、处理和存储您的个人信息。您可以选择不同意修订后的《数据资产运营平台隐私政策》,但可能导致您无法使用数据资产运营平台的全部或部分产品及服务功能。
</p>
<p>(三)未经您同意,我们不会限制您按照本隐私政策所应享受的权利。</p>
<p>(四)发生下列重大变化情形时,我们会适时对本隐私政策进行更新:</p>
<p style="text-indent:2em;">1.我们的基本情况发生变化,包括兼并、收购、重组引起的所有者变更;</p>
<p style="text-indent:2em;">2.收集、存储、使用个人信息的范围、目的、规则发生变化;</p>
<p style="text-indent:2em;">3.对外提供个人信息的对象、范围、目的发生变化;</p>
<p style="text-indent:2em;">4.您访问和管理个人信息的方式发生变化;</p>
<p style="text-indent:2em;">5.数据安全能力、信息安全风险发生变化;</p>
<p style="text-indent:2em;">6.用户询问、投诉的渠道和机制,以及外部纠纷解决机构及联络方式发生变化; </p>
<p style="text-indent:2em;">7.其他可能对您的个人信息权益产生重大影响的变化。</p>
<p>(五)如果您在本隐私政策更新生效后继续使用相关服务,即表示您已充分阅读、理解并接受更新后的政策并愿意受更新后的政策约束。</p>
<h3>7、如何联系我们</h3>
<p>
(一)如果您对本隐私政策存在任何疑问,或对于您的个人信息处理存在任何投诉、意见,请通过相应的邮箱、客服联系我们,我们会尽快回复,最长不超过15天。
</p>
<p>(二)如果您对我们的回复不满意,特别是您认为我们的个人信息处理行为损害了您的合法权益,您还可以通过向数据资产运营平台住所地有管辖权的法院提起诉讼来寻求解决方案。
</p>
<p>(三)产品运营者基本情况</p>
<p>产品名称:数据资产运营平台</p>
<p>公司名称:北京传世博润科技有限公司</p>
<p>
注册地址:北京市朝阳区利泽西街6号院3号楼10层1001内18
</p>
<p>个人信息保护相关负责人联系方式: 027-5990-7980</p>
</div>
</div>
</template>
<script>
export default {};
</script>
......@@ -35,11 +35,11 @@ export default ({ mode, command }) => {
changeOrigin: env.VITE_OPEN_PROXY === 'true',
rewrite: path => path.replace(/\/api/, ''),
},
'/portal':{
target: env.VITE_API_PORTALURL,
changeOrigin: env.VITE_OPEN_PROXY === 'true',
rewrite: path => path.replace(/\/portal/, ''),
},
// '/portal':{
// target: env.VITE_API_PORTALURL,
// changeOrigin: env.VITE_OPEN_PROXY === 'true',
// rewrite: path => path.replace(/\/portal/, ''),
// },
'/circulation':{
target: env.VITE_APP_CIRCULATION,
changeOrigin: env.VITE_OPEN_PROXY === 'true',
......
Styling with Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!