perf(仪表板): 分享 Ticket增加分页机制

This commit is contained in:
fit2cloud-chenyw
2024-12-17 19:15:50 +08:00
parent 73277a7fe2
commit a1a0780ed4
15 changed files with 1091 additions and 394 deletions

View File

@@ -2,6 +2,8 @@ package io.dataease.share.dao.ext.mapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import io.dataease.api.xpack.share.vo.TicketVO;
import io.dataease.share.dao.auto.entity.CoreShareTicket;
import io.dataease.share.dao.ext.po.XpackSharePO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@@ -32,4 +34,10 @@ public interface XpackShareExtMapper {
@Update("update core_share_ticket set uuid = #{ticketUuid} where uuid = #{originUuid}")
void updateTicketUuid(@Param("originUuid") String originUuid, @Param("ticketUuid") String ticketUuid);
@Select("""
select * from core_share_ticket
${ew.customSqlSegment}
""")
IPage<CoreShareTicket> pager(IPage<TicketVO> page, @Param("ew") QueryWrapper<CoreShareTicket> ew);
}

View File

@@ -1,6 +1,8 @@
package io.dataease.share.manage;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.dataease.api.xpack.share.request.TicketCreator;
import io.dataease.api.xpack.share.request.TicketDelRequest;
import io.dataease.api.xpack.share.request.TicketSwitchRequest;
@@ -15,15 +17,16 @@ import io.dataease.share.dao.auto.mapper.XpackShareMapper;
import io.dataease.share.dao.ext.mapper.XpackShareExtMapper;
import io.dataease.utils.AuthUtils;
import io.dataease.utils.BeanUtils;
import io.dataease.utils.CommonBeanFactory;
import io.dataease.utils.IDUtils;
import jakarta.annotation.Resource;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Objects;
@Component
public class ShareTicketManage {
@@ -37,12 +40,14 @@ public class ShareTicketManage {
@Resource
private XpackShareExtMapper xpackShareExtMapper;
public CoreShareTicket getByTicket(String ticket) {
QueryWrapper<CoreShareTicket> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("ticket", ticket);
return coreShareTicketMapper.selectOne(queryWrapper);
}
@Transactional
public String saveTicket(TicketCreator creator) {
String ticket = creator.getTicket();
if (StringUtils.isNotBlank(ticket)) {
@@ -51,6 +56,9 @@ public class ShareTicketManage {
if (creator.isGenerateNew()) {
ticketEntity.setAccessTime(null);
ticketEntity.setTicket(CodingUtil.shortUuid());
coreShareTicketMapper.deleteById(ticketEntity);
coreShareTicketMapper.insert(ticketEntity);
return ticketEntity.getTicket();
}
ticketEntity.setArgs(creator.getArgs());
ticketEntity.setExp(creator.getExp());
@@ -60,17 +68,23 @@ public class ShareTicketManage {
return ticketEntity.getTicket();
}
}
ticket = CodingUtil.shortUuid();
if (StringUtils.isBlank(ticket)) {
ticket = CodingUtil.shortUuid();
}
CoreShareTicket linkTicket = new CoreShareTicket();
linkTicket.setId(IDUtils.snowID());
linkTicket.setTicket(ticket);
linkTicket.setArgs(creator.getArgs());
linkTicket.setExp(creator.getExp());
linkTicket.setUuid(creator.getUuid());
coreShareTicketMapper.insert(linkTicket);
Objects.requireNonNull(CommonBeanFactory.proxy(this.getClass())).saveDao(linkTicket);
return ticket;
}
public void saveDao(CoreShareTicket ticket) {
coreShareTicketMapper.insert(ticket);
}
public void deleteTicket(TicketDelRequest request) {
String ticket = request.getTicket();
if (StringUtils.isBlank(ticket)) {
@@ -92,7 +106,7 @@ public class ShareTicketManage {
xpackShareMapper.updateById(xpackShare);
}
public List<TicketVO> query(Long resourceId) {
public IPage<TicketVO> query(Long resourceId, Page<TicketVO> page) {
QueryWrapper<XpackShare> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("resource_id", resourceId);
queryWrapper.eq("creator", AuthUtils.getUser().getUserId());
@@ -102,9 +116,16 @@ public class ShareTicketManage {
if (StringUtils.isBlank(uuid)) return null;
QueryWrapper<CoreShareTicket> ticketQueryWrapper = new QueryWrapper<>();
ticketQueryWrapper.eq("uuid", uuid);
List<CoreShareTicket> coreShareTickets = coreShareTicketMapper.selectList(ticketQueryWrapper);
if (CollectionUtils.isEmpty(coreShareTickets)) return null;
return coreShareTickets.stream().map(item -> BeanUtils.copyBean(new TicketVO(), item)).toList();
IPage<CoreShareTicket> pager = xpackShareExtMapper.pager(page, ticketQueryWrapper);
List<CoreShareTicket> records = pager.getRecords();
IPage<TicketVO> iPage = new Page<>();
iPage.setPages(pager.getPages());
iPage.setTotal(pager.getTotal());
iPage.setCurrent(pager.getCurrent());
iPage.setSize(pager.getSize());
List<TicketVO> vos = records.stream().map(record -> BeanUtils.copyBean(new TicketVO(), record)).toList();
iPage.setRecords(vos);
return iPage;
}
@Transactional
@@ -151,4 +172,14 @@ public class ShareTicketManage {
vo.setTicketExp(time > expTime);
return vo;
}
public Integer getLimit() {
return 0;
}
public long ticketCount(String uuid) {
QueryWrapper<CoreShareTicket> ticketQueryWrapper = new QueryWrapper<>();
ticketQueryWrapper.eq("uuid", uuid);
return coreShareTicketMapper.selectCount(ticketQueryWrapper);
}
}

View File

@@ -1,15 +1,17 @@
package io.dataease.share.server;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.dataease.api.xpack.share.ShareTicketApi;
import io.dataease.api.xpack.share.request.TicketCreator;
import io.dataease.api.xpack.share.request.TicketDelRequest;
import io.dataease.api.xpack.share.request.TicketSwitchRequest;
import io.dataease.api.xpack.share.vo.TicketVO;
import io.dataease.commons.utils.CodingUtil;
import io.dataease.share.manage.ShareTicketManage;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/ticket")
@@ -34,7 +36,18 @@ public class ShareTicketServer implements ShareTicketApi {
}
@Override
public List<TicketVO> query(Long resourceId) {
return shareTicketManage.query(resourceId);
public IPage<TicketVO> pager(Long resourceId, int goPage, int pageSize) {
Page<TicketVO> page = new Page<>(goPage, pageSize);
return shareTicketManage.query(resourceId, page);
}
@Override
public String tempTicket() {
return CodingUtil.shortUuid();
}
@Override
public Integer limit() {
return shareTicketManage.getLimit();
}
}

View File

@@ -15,7 +15,8 @@ const props = defineProps({
tableData: propTypes.array,
emptyDesc: propTypes.string,
emptyImg: propTypes.string,
border: propTypes.bool.def(false)
border: propTypes.bool.def(false),
showEmptyImg: propTypes.bool.def(true)
})
const attrs = useAttrs()
@@ -136,6 +137,7 @@ defineExpose({
</table-body>
<template #empty>
<empty-background
v-if="props.showEmptyImg"
:description="props.emptyDesc ? props.emptyDesc : '暂无数据'"
:img-type="imgType || 'noneWhite'"
/>

View File

@@ -3475,7 +3475,10 @@ Scatter chart (bubble) chart: {a} (series name), {b} (data name), {c} (value arr
back: 'Return to the public link settings page',
refresh: 'Refresh',
time_tips:
'Unit: minutes, range: [0-1440], 0 means no time limit, starting from the first access using the ticket'
'Unit: minutes, range: [0-1440], 0 means no time limit, starting from the first access using the ticket',
arg_val_tips: 'Please enter parameter values',
arg_format_tips:
'Please use JSON format string, example single valued argVal, multi valued [argVal1, argVal2]'
},
pblink: {
key_pwd: 'Please enter the password to open the link',

View File

@@ -3384,7 +3384,9 @@ export default {
require: '必選',
back: '返回公共連結設定頁',
refresh: '刷新',
time_tips: '單位: 分鐘,範圍: [0-1440],0代表無期限自首次使用ticket訪問開始'
time_tips: '單位: 分鐘,範圍: [0-1440],0代表無期限自首次使用ticket訪問開始',
arg_val_tips: '請輸入參數值',
arg_format_tips: '請使用JSON格式字符串示例單值argVal多值[argVal1, argVal2]'
},
pblink: {
key_pwd: '請輸入密碼開啟連結',

View File

@@ -3387,7 +3387,9 @@ export default {
require: '必选',
back: '返回公共链接设置页面',
refresh: '刷新',
time_tips: '单位: 分钟,范围: [0-1440],0代表无期限自首次使用ticket访问开始'
time_tips: '单位: 分钟,范围: [0-1440],0代表无期限自首次使用ticket访问开始',
arg_val_tips: '请输入参数值',
arg_format_tips: '请使用JSON格式字符串示例单值argVal多值[argVal1, argVal2]'
},
pblink: {
key_pwd: '请输入密码打开链接',

View File

@@ -0,0 +1,115 @@
<template>
<div class="link-pwd-dialog-container">
<el-dialog
v-model="dialogVisible"
:title="t('user.change_password')"
width="420"
:append-to-body="true"
:before-close="handleClose"
>
<div class="link-pwd-container">
<el-form ref="pwdForm" :model="state.form" :rules="rule" label-position="top">
<el-form-item :label="t('system.new_password')" prop="pwd">
<el-input v-model="state.form.pwd" :placeholder="t('commons.input_password')" />
<div class="tips ed-form-item__error">
{{ t('work_branch.password_hint') }}
</div>
</el-form-item>
</el-form>
</div>
<template #footer>
<span class="dialog-footer">
<el-button secondary @click.stop="cancel">{{ t('common.cancel') }}</el-button>
<el-button type="primary" @click.stop="save">
{{ t('common.save') }}
</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
const { t } = useI18n()
const dialogVisible = ref(false)
const pwdForm = ref()
const originPwd = ref('')
const state = reactive({
form: reactive<any>({
pwd: ''
})
})
const rule = reactive<any>({
pwd: [
{ required: true, message: t('work_branch.password_null_hint'), trigger: 'blur' },
{
pattern: /^(?=.*[A-Za-z])(?=.*\d)(?=.*[!@#$%^&*()_+])[A-Za-z\d!@#$%^&*()_+]{4,10}$/,
message: t('work_branch.password_hint'),
trigger: 'blur'
}
]
})
const handleClose = done => {
state.form.pwd = ''
originPwd.value = ''
pwdForm.value.resetFields()
done()
}
const cancel = () => {
dialogVisible.value = false
}
const emits = defineEmits(['pwdChange'])
const save = () => {
const formEl = pwdForm.value
if (!formEl) return
formEl.validate(valid => {
if (valid) {
if (originPwd.value !== state.form.pwd) {
emits('pwdChange', state.form.pwd)
}
cancel()
}
})
}
const open = (pwd: string) => {
state.form.pwd = pwd
originPwd.value = pwd
dialogVisible.value = true
}
defineExpose({
open
})
</script>
<style lang="less" scoped>
.link-pwd-container {
width: 100%;
:deep(.ed-form-item) {
margin-bottom: 2px;
height: 108px;
}
:deep(.ed-form-item__label) {
line-height: 22px !important;
height: 22px;
}
:deep(.ed-form-item__error:not(.tips)) {
display: none;
}
.tips {
color: #8f959e;
line-height: 22px;
font-size: 14px;
font-weight: 400;
}
:deep(.is-error) {
.tips {
color: red !important;
}
}
}
</style>

View File

@@ -17,8 +17,7 @@
v-if="dialogVisible && props.weight >= 7"
class="copy-link_dialog"
:class="{
'hidden-footer': !shareEnable || showTicket,
'is-ticket-dialog': shareEnable && showTicket
'hidden-footer': !shareEnable
}"
v-model="dialogVisible"
:close-on-click-modal="true"
@@ -28,7 +27,7 @@
width="480px"
:show-close="false"
>
<div class="share-dialog-container" :class="{ 'hidden-link-container': showTicket }">
<div class="share-dialog-container">
<div class="copy-link">
<div class="open-share flex-align-center">
<el-switch size="small" v-model="shareEnable" @change="enableSwitcher" />
@@ -36,24 +35,53 @@
</div>
<div v-if="shareEnable" class="custom-link-line">
<el-input
:class="!linkCustom ? 'link-input-readlonly' : ''"
ref="linkUuidRef"
placeholder=""
v-model="state.detailInfo.uuid"
:disabled="!linkCustom"
@blur="finishEditUuid"
:readonly="!linkCustom"
@blur="validateUuid"
>
<template v-if="!linkCustom" #prefix>
{{ formatLinkBase() }}
</template>
</el-input>
<el-button v-if="linkCustom" text @click="finishEditUuid">{{
t('components.complete')
}}</el-button>
<el-button v-else @click="editUuid" size="default" plain>
<template #icon>
<icon name="icon_admin_outlined"><icon_admin_outlined class="svg-icon" /></icon>
<template #suffix>
<div class="share-input-suffix">
<span class="suffix-split" />
<div class="input-suffix-btn" v-if="!linkCustom" @click="editUuid">
<el-tooltip
class="item"
effect="dark"
:content="t('commons.edit') + t('chart.indicator_suffix')"
placement="top"
>
<Icon name="icon_edit_outlined"><icon_edit_outlined class="svg-icon" /></Icon>
</el-tooltip>
</div>
<div class="input-suffix-btn" v-if="linkCustom" @click.stop="resetUuid">
<el-tooltip
class="item"
effect="dark"
:content="t('commons.cancel')"
placement="top"
>
<Icon name="icon_close_outlined"><icon_close_outlined class="svg-icon" /></Icon>
</el-tooltip>
</div>
<div class="input-suffix-btn done" v-if="linkCustom" @click="finishEditUuid">
<el-tooltip
class="item"
effect="dark"
:content="t('commons.save')"
placement="top"
>
<Icon name="icon_done_outlined"><icon_done_outlined class="svg-icon" /></Icon>
</el-tooltip>
</div>
</div>
</template>
</el-button>
</el-input>
</div>
<div v-if="shareEnable" class="exp-container">
@@ -73,7 +101,6 @@
<div class="inline-share-item-picker">
<el-date-picker
:clearable="false"
size="small"
v-if="state.detailInfo.exp"
class="share-exp-picker"
v-model="state.detailInfo.exp"
@@ -112,39 +139,44 @@
<div class="inline-share-item" v-if="passwdEnable">
<el-input
ref="pwdRef"
class="link-input-readlonly"
v-model="state.detailInfo.pwd"
:readonly="state.detailInfo.autoPwd"
size="small"
@blur="validatePwdFormat"
readonly
>
<template #append>
<div class="share-pwd-opt">
<div
v-if="state.detailInfo.autoPwd"
@click.stop="resetPwd"
class="share-reset-container"
>
<span>{{ t('commons.reset') }}</span>
<template #suffix>
<div class="share-input-suffix">
<span class="suffix-split" />
<div class="input-suffix-btn" @click="copyPwd">
<el-tooltip
class="item"
effect="dark"
:content="t('commons.copy')"
placement="top"
>
<Icon name="de-copy"><deCopy class="svg-icon" /></Icon>
</el-tooltip>
</div>
<div @click.stop="copyPwd" class="share-reset-container">
<span>{{ t('commons.copy') }}</span>
<div class="input-suffix-btn" @click="resetPwd">
<el-tooltip
class="item"
effect="dark"
:content="t('commons.reset')"
placement="top"
>
<Icon name="icon_refresh_outlined"
><icon_refresh_outlined class="svg-icon"
/></Icon>
</el-tooltip>
</div>
</div>
</template>
</el-input>
<el-button secondary @click="openPwdDialog">{{ t('user.change_password') }}</el-button>
</div>
</div>
</div>
</div>
<div v-if="shareEnable && showTicket" class="share-ticket-container">
<share-ticket
:uuid="state.detailInfo.uuid"
:resource-id="props.resourceId"
:ticket-require="state.detailInfo.ticketRequire"
@require-change="updateRequireTicket"
@close="closeTicket"
/>
</div>
<template #footer>
<span class="dialog-footer">
<el-button secondary @click="openTicket">{{ t('work_branch.ticket_setting') }}</el-button>
@@ -154,12 +186,28 @@
</span>
</template>
</el-dialog>
<custom-link-pwd ref="customPwdRef" @pwd-change="customPwdChange" />
<ticket-dialog ref="ticketDialogRef">
<div v-if="shareEnable && showTicket">
<share-ticket
:uuid="state.detailInfo.uuid"
:resource-id="props.resourceId"
:ticket-require="state.detailInfo.ticketRequire"
@require-change="updateRequireTicket"
@close="closeTicket"
/>
</div>
</ticket-dialog>
</template>
<script lang="ts" setup>
import dvShare from '@/assets/svg/dv-share.svg'
import icon_shareLabel_outlined from '@/assets/svg/icon_share-label_outlined.svg'
import icon_admin_outlined from '@/assets/svg/icon_admin_outlined.svg'
import deCopy from '@/assets/svg/de-copy.svg'
import icon_refresh_outlined from '@/assets/svg/icon_refresh_outlined.svg'
import icon_edit_outlined from '@/assets/svg/icon_edit_outlined.svg'
import icon_close_outlined from '@/assets/svg/icon_close_outlined.svg'
import icon_done_outlined from '@/assets/svg/icon_done_outlined.svg'
import { useI18n } from '@/hooks/web/useI18n'
import { ref, reactive, onMounted, computed, nextTick } from 'vue'
import request from '@/config/axios'
@@ -170,6 +218,9 @@ import useClipboard from 'vue-clipboard3'
import ShareTicket from './ShareTicket.vue'
import { useEmbedded } from '@/store/modules/embedded'
import { useShareStoreWithOut } from '@/store/modules/share'
import CustomLinkPwd from './CustomLinkPwd.vue'
import TicketDialog from './TicketDialog.vue'
const shareStore = useShareStoreWithOut()
const embeddedStore = useEmbedded()
const { toClipboard } = useClipboard()
@@ -181,6 +232,9 @@ const props = defineProps({
weight: propTypes.number.def(0),
isButton: propTypes.bool.def(false)
})
const originUuid = ref('')
const customPwdRef = ref()
const ticketDialogRef = ref()
const pwdRef = ref(null)
const expCheckbox = ref()
const pwdCheckbox = ref()
@@ -251,6 +305,11 @@ const finishEditUuid = async () => {
const uuidValid = await validateUuid()
linkCustom.value = !uuidValid
}
const resetUuid = event => {
event.stopPropagation()
state.detailInfo.uuid = originUuid.value
finishEditUuid()
}
const copyPwd = async () => {
if (shareEnable.value && passwdEnable.value) {
if (!state.detailInfo.autoPwd && existErrorMsg('link-pwd-error-msg')) {
@@ -320,6 +379,7 @@ const loadShareInfo = cb => {
.get({ url })
.then(res => {
state.detailInfo = { ...res.data }
originUuid.value = res.data.uuid
setPageInfo()
})
.finally(() => {
@@ -400,9 +460,8 @@ const beforeClose = async done => {
return
}
}
const pwdValid = validatePwdFormat()
const uuidValid = await validateUuid()
if (pwdValid && uuidValid) {
if (uuidValid) {
showTicket.value = false
done()
}
@@ -433,26 +492,6 @@ const validatePwdRequire = () => {
showCheckboxError(t('common.required'), pwdCheckbox)
return false
}
const validatePwdFormat = () => {
if (!shareEnable.value || state.detailInfo.autoPwd) {
showPageError(null, pwdRef)
return true
}
const val = state.detailInfo.pwd
if (!val) {
showPageError(t('work_branch.password_null_hint'), pwdRef)
return false
}
const regex = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[!@#$%^&*()_+])[A-Za-z\d!@#$%^&*()_+]{4,10}$/
const regep = new RegExp(regex)
if (!regep.test(val)) {
showPageError(t('work_branch.password_hint'), pwdRef)
return false
}
showPageError(null, pwdRef)
resetPwdHandler(val, false)
return true
}
const showCheckboxError = (msg, target, className?: string) => {
if (!target.value) {
return
@@ -575,8 +614,10 @@ const getUuid = () => {
const openTicket = () => {
showTicket.value = true
ticketDialogRef.value.open()
}
const closeTicket = () => {
ticketDialogRef.value.close()
showTicket.value = false
}
const updateRequireTicket = val => {
@@ -586,6 +627,15 @@ const updateRequireTicket = val => {
const execute = () => {
share()
}
const openPwdDialog = () => {
customPwdRef.value.open(state.detailInfo.pwd)
}
const customPwdChange = val => {
state.detailInfo.pwd = val
resetPwdHandler(val, false)
}
defineExpose({
execute
})
@@ -660,45 +710,10 @@ onMounted(() => {
margin-right: 10px;
}
.inline-share-item {
display: inline-flex;
column-gap: 12px;
margin-left: 25px;
width: 220px;
:deep(.ed-input-group__append) {
width: initial !important;
background: none;
color: #1f2329;
padding: 0px 0px !important;
.share-pwd-opt {
display: flex;
padding: 1px;
.share-reset-container {
&:not(:first-child) {
border-left: 1px solid var(--ed-input-border-color) !important;
}
width: 45px;
display: flex;
justify-content: center;
&:hover {
cursor: pointer;
background-color: #f5f6f7;
}
&:active {
cursor: pointer;
background-color: #eff0f1;
}
}
}
}
:deep(.link-pwd-error-msg) {
color: red;
position: absolute;
z-index: 9;
font-size: 10px;
height: 10px;
top: 21px;
width: 350px;
left: 0px;
}
width: 332px;
}
}
@@ -737,9 +752,6 @@ onMounted(() => {
}
}
}
.hidden-link-container {
display: none;
}
}
:deep(.checkbox-span) {
display: flex;
@@ -757,4 +769,52 @@ onMounted(() => {
display: none;
}
}
.share-input-suffix {
display: flex;
height: 30px;
line-height: 30px;
column-gap: 4px;
align-items: center;
.suffix-split {
height: 30px;
width: 1px;
display: inline-block;
background-color: #bbbfc4;
margin-right: 4px;
}
.done {
color: #3370ff;
&:hover {
background-color: #3370ff1a !important;
}
}
.input-suffix-btn {
width: 24px;
height: 24px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
&:hover {
background-color: #1f23291a;
}
svg {
width: 16px;
height: 16px;
}
}
}
.link-input-readlonly {
:deep(.ed-input__wrapper) {
background-color: rgba(0, 0, 0, 0.1);
color: #8f959e;
&:hover {
box-shadow: 0 0 0 1px var(--ed-input-border-color, var(--ed-border-color)) inset;
}
input {
color: #646a73;
}
}
}
</style>

View File

@@ -2,15 +2,6 @@
<div class="ticket">
<div class="ticket-model">
<div class="ticket-model-start">
<el-tooltip class="item" effect="dark" :content="$t('link_ticket.back')" placement="top">
<span class="back-tips">
<el-icon class="custom-el-icon back-icon" @click.stop="close">
<Icon class="toolbar-icon" name="icon_left_outlined"
><icon_left_outlined class="svg-icon toolbar-icon"
/></Icon>
</el-icon>
</span>
</el-tooltip>
<span class="ticket-title">{{ 'Ticket ' + t('commons.setting') }}</span>
</div>
<div class="ticket-model-end">
@@ -19,6 +10,11 @@
@change="requireTicketChange"
:label="t('link_ticket.require')"
/>
<span class="top-split" />
<span class="top-close" @click.stop="finish">
<icon name="icon_close_outlined"><icon_close_outlined class="svg-icon" /></icon>
</span>
</div>
</div>
<div class="ticket-add">
@@ -26,12 +22,20 @@
<template #icon>
<icon name="icon_add_outlined"><icon_add_outlined class="svg-icon" /></icon>
</template>
{{ t('commons.create') }}
{{ t('commons.add') }}
</el-button>
</div>
<div class="ticket-table">
<el-table :data="state.tableData" style="width: 100%" size="small">
<el-table-column prop="ticket" label="Ticket" width="130">
<grid-table
ref="multipleTableRef"
:show-empty-img="false"
:table-data="state.tableData"
:pagination="state.paginationConfig"
class="popper-max-width"
@current-change="pageChange"
@size-change="sizeChange"
>
<el-table-column prop="ticket" label="Ticket">
<template v-slot="scope">
<div class="ticket-row">
<span :title="scope.row.ticket">{{ scope.row.ticket }}</span>
@@ -60,7 +64,7 @@
</template>
</el-table-column>
<el-table-column prop="exp" :label="$t('visualization.over_time')" width="100">
<el-table-column prop="exp" :label="$t('visualization.over_time')" width="117">
<template v-slot:header>
<div class="ticket-exp-head">
<span>{{ t('visualization.over_time') }}</span>
@@ -77,43 +81,31 @@
</div>
</template>
<template v-slot="scope">
<el-input
v-if="scope.row.isEdit"
:ref="el => setExpRef(el, scope.$index)"
v-model="scope.row.exp"
type="number"
:placeholder="$t('commons.input_content')"
min="0"
max="1440"
size="small"
@input="v => handleInput(v, scope.$index)"
@change="val => validateExp(val, scope.$index)"
/>
<span v-else>
<span>
{{ scope.row.exp }}
</span>
</template>
</el-table-column>
<el-table-column prop="args" :label="$t('dataset.param')">
<el-table-column prop="args" :label="$t('dataset.param')" width="117">
<template v-slot="scope">
<el-input
v-if="scope.row.isEdit"
:ref="el => setArgRef(el, scope.$index)"
v-model="scope.row.args"
type="text"
:placeholder="$t('commons.input_content')"
maxlength="200"
size="small"
@change="val => validateArgs(val, scope.$index)"
/>
<span v-else>
{{ scope.row.args || '-' }}
</span>
<el-tooltip class="box-item" effect="light" :content="scope.row.args" placement="top">
<span style="color: #3370ff">
{{ getArgCount(scope.row) }}
</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column :label="$t('commons.operating')" width="80">
<template v-slot="scope">
<div class="ticket-row">
<el-tooltip class="item" effect="dark" :content="$t('commons.edit')" placement="top">
<el-button text @click.stop="editRow(scope.row)">
<template #icon>
<Icon name="icon_edit_outlined"><icon_edit_outlined class="svg-icon" /></Icon>
</template>
</el-button>
</el-tooltip>
<el-tooltip class="item" effect="dark" :content="t('commons.delete')" placement="top">
<el-button text @click.stop="deleteTicket(scope.row, scope.$index)">
<template #icon>
@@ -123,43 +115,23 @@
</template>
</el-button>
</el-tooltip>
<el-tooltip
class="item"
effect="dark"
:content="scope.row.isEdit ? $t('commons.save') : $t('commons.edit')"
placement="top"
>
<el-button v-if="!scope.row.isEdit" text @click.stop="editRow(scope.row)">
<template #icon>
<Icon name="icon_edit_outlined"><icon_edit_outlined class="svg-icon" /></Icon>
</template>
</el-button>
<el-button v-else text @click.stop="saveRow(scope.row, scope.$index)">
<template #icon>
<Icon name="edit-done"><editDone class="svg-icon" /></Icon>
</template>
</el-button>
</el-tooltip>
</div>
</template>
</el-table-column>
</el-table>
</div>
<div class="ticket-btn">
<el-button type="primary" @click.stop="finish"> {{ $t('components.complete') }} </el-button>
</grid-table>
</div>
</div>
<ticket-edit ref="ticketEditor" :uuid="props.uuid" @saved="loadTicketData" />
</template>
<script lang="ts" setup>
import icon_left_outlined from '@/assets/svg/icon_left_outlined.svg'
import icon_close_outlined from '@/assets/svg/icon_close_outlined.svg'
import deCopy from '@/assets/svg/de-copy.svg'
import icon_refresh_outlined from '@/assets/svg/icon_refresh_outlined.svg'
import dvInfo from '@/assets/svg/dv-info.svg'
import icon_deleteTrash_outlined from '@/assets/svg/icon_delete-trash_outlined.svg'
import icon_edit_outlined from '@/assets/svg/icon_edit_outlined.svg'
import editDone from '@/assets/svg/edit-done.svg'
import icon_add_outlined from '@/assets/svg/icon_add_outlined.svg'
import { ref, reactive, onMounted, toRefs } from 'vue'
import { propTypes } from '@/utils/propTypes'
@@ -169,7 +141,8 @@ import { ElMessage, ElMessageBox } from 'element-plus-secondary'
import useClipboard from 'vue-clipboard3'
import { useEmbedded } from '@/store/modules/embedded'
import { SHARE_BASE } from './option'
import GridTable from '@/components/grid-table/src/GridTable.vue'
import TicketEdit from './TicketEdit.vue'
const embeddedStore = useEmbedded()
const { toClipboard } = useClipboard()
const { t } = useI18n()
@@ -178,13 +151,16 @@ const props = defineProps({
resourceId: propTypes.string.def(null),
ticketRequire: propTypes.bool
})
const ticketEditor = ref()
const { ticketRequire } = toRefs(props)
const expRefs = ref({})
const argRefs = ref({})
const state = reactive({
tableData: []
tableData: [],
paginationConfig: {
currentPage: 1,
pageSize: 10,
total: 0
}
})
const emits = defineEmits(['close', 'requireChange'])
@@ -192,18 +168,6 @@ const close = () => {
emits('close')
}
const setExpRef = (el, index) => {
if (el) {
expRefs.value[index] = el
}
}
const setArgRef = (el, index) => {
if (el) {
argRefs.value[index] = el
}
}
const requireTicketChange = val => {
const url = '/ticket/enableTicket'
const data = {
@@ -231,22 +195,21 @@ const createLimit = (count?: number) => {
}
return true
}
const getArgCount = row => {
const args = row.args
if (!args) {
return 0
}
try {
const obj = JSON.parse(args)
return Object.keys(obj).length
} catch (error) {
console.error(error)
return 0
}
}
const addRow = () => {
if (!createLimit()) {
return
}
const row = {
ticket: '',
exp: 30,
args: '',
uuid: props.uuid
}
const url = '/ticket/saveTicket'
request.post({ url, data: row }).then(res => {
row.ticket = res.data
row['isEdit'] = false
state.tableData.splice(0, 0, row)
})
ticketEditor.value.edit(null, formatLinkAddr())
}
const formatLinkAddr = () => {
return formatLinkBase() + props.uuid
@@ -282,64 +245,6 @@ const refreshTicket = row => {
row.ticket = res.data
})
}
const handleInput = (val, index) => {
if (val === null || val === '') {
return
}
state.tableData[index]['exp'] = val.replace(/[^\d]/g, '')
}
const validateExp = (val, index) => {
const cref = expRefs.value[index]
const e = cref.input
if (val === null || val === '' || typeof val === 'undefined') {
state.tableData[index]['exp'] = 0
return true
}
if (val > 1440 || val < 0) {
e.style.color = 'red'
e.parentNode.setAttribute('style', 'box-shadow: 0 0 0 1px red inset;')
return false
} else {
e.style.color = null
e.parentNode.removeAttribute('style')
return true
}
}
const validateArgs = (val, index) => {
const cref = argRefs.value[index]
const e = cref.input
if (val === null || val === '' || typeof val === 'undefined') {
e.style.color = null
e.parentNode.removeAttribute('style')
const child = e.parentNode.querySelector('.error-msg')
if (child) {
e.parentNode.removeChild(child)
}
return true
}
try {
JSON.parse(val)
e.style.color = null
e.parentNode.removeAttribute('style')
const child = e.parentNode.querySelector('.error-msg')
if (child) {
e.parentNode.removeChild(child)
}
return true
} catch (error) {
e.style.color = 'red'
e.parentNode.setAttribute('style', 'box-shadow: 0 0 0 1px red inset;')
const child = e.parentNode.querySelector('.error-msg')
if (!child) {
const errorDom = document.createElement('div')
errorDom.className = 'error-msg'
errorDom.innerText = '格式错误'
e.parentNode.appendChild(errorDom)
}
return false
}
}
const deleteTicket = (row, index) => {
const param = { ticket: row.ticket }
@@ -349,29 +254,32 @@ const deleteTicket = (row, index) => {
})
}
const saveRow = (row, index) => {
const url = '/ticket/saveTicket'
validateExp(row.exp, index) &&
validateArgs(row.args, index) &&
request.post({ url, data: row }).then(() => {
row.isEdit = false
})
}
const editRow = row => {
row.isEdit = true
ticketEditor.value.edit(row, formatLinkAddr())
}
const finish = () => {
close()
}
const loadTicketData = () => {
const resourceId = props.resourceId
const url = `/ticket/query/${resourceId}`
request.get({ url }).then(res => {
state.tableData = res.data || []
const url = `/ticket/pager/${resourceId}/${state.paginationConfig.currentPage}/${state.paginationConfig.pageSize}`
request.post({ url }).then(res => {
state.tableData = res.data?.records || []
state.paginationConfig.total = res.data.total
})
}
const pageChange = index => {
if (typeof index !== 'number') {
return
}
state.paginationConfig.currentPage = index
loadTicketData()
}
const sizeChange = size => {
state.paginationConfig.pageSize = size
loadTicketData()
}
onMounted(() => {
loadTicketData()
})
@@ -379,7 +287,8 @@ onMounted(() => {
<style lang="less" scoped>
.ticket {
min-height: 280px;
height: auto;
max-height: 560px;
.ticket-model {
display: flex;
height: 22px;
@@ -415,10 +324,35 @@ onMounted(() => {
}
.ticket-model-end {
display: flex;
align-items: center;
label {
height: 22px;
margin-right: 8px;
}
.top-split {
height: 18px;
width: 1px;
display: inline-block;
background-color: #bbbfc4;
margin: 0 20px;
}
.top-close {
height: 22px;
line-height: 22px;
display: inline-flex;
align-items: center;
justify-content: center;
color: #646a73;
&:hover {
cursor: pointer;
background-color: #1f23291a;
border-radius: 4px;
}
svg {
width: 20px;
height: 20px;
}
}
}
}
.ticket-add {
@@ -437,15 +371,6 @@ onMounted(() => {
overflow-y: overlay;
position: relative;
height: calc(100% - 124px);
:deep(.error-msg) {
color: red;
position: fixed;
z-index: 9;
font-size: 10px;
height: 10px;
margin-bottom: 12px;
margin-right: -80px;
}
:deep(.ticket-exp-head) {
display: flex;
line-height: 22px;
@@ -466,7 +391,7 @@ onMounted(() => {
align-items: center;
height: 22px;
span {
width: 66px;
width: 126px;
margin-right: 8px;
overflow: hidden;
white-space: nowrap;
@@ -511,14 +436,6 @@ onMounted(() => {
}
}
}
:deep(.ed-input__inner) {
height: 18px;
line-height: 18px;
}
}
.ticket-btn {
margin: 16px 0;
float: right;
}
}
</style>

View File

@@ -5,7 +5,7 @@
width="480"
placement="bottom-end"
:show-arrow="false"
:popper-class="`share-popover ${showTicket ? 'share-ticket-popover' : ''}`"
popper-class="share-popover"
@show="share"
>
<template #reference>
@@ -23,11 +23,7 @@
{{ t('visualization.share') }}
</el-button>
</template>
<div
v-if="!shareDisable"
class="share-container"
:class="{ 'hidden-link-container': showTicket }"
>
<div v-if="!shareDisable" class="share-container">
<div class="share-title share-padding">{{ t('work_branch.public_link_share') }}</div>
<div class="open-share flex-align-center share-padding">
<el-switch size="small" v-model="shareEnable" @change="enableSwitcher" />
@@ -37,23 +33,48 @@
<el-input
ref="linkUuidRef"
placeholder=""
:class="!linkCustom && 'maxW380'"
:class="!linkCustom ? 'link-input-readlonly' : ''"
v-model="state.detailInfo.uuid"
:disabled="!linkCustom"
@blur="finishEditUuid"
:readonly="!linkCustom"
@blur="validateUuid"
>
<template v-if="!linkCustom" #prefix>
{{ formatLinkBase() }}
</template>
</el-input>
<el-button v-if="linkCustom" text @click.stop="finishEditUuid">{{
t('components.complete')
}}</el-button>
<el-button v-else @click.stop="editUuid" size="default" plain>
<template #icon>
<icon name="icon_admin_outlined"><icon_admin_outlined class="svg-icon" /></icon>
<template #suffix>
<div class="share-input-suffix">
<span class="suffix-split" />
<div class="input-suffix-btn edit-uuid-icon" v-if="!linkCustom" @click="editUuid">
<el-tooltip
class="item"
effect="dark"
:content="t('commons.edit') + t('chart.indicator_suffix')"
placement="top"
>
<Icon name="icon_edit_outlined" class="edit-uuid-icon">
<icon_edit_outlined class="svg-icon edit-uuid-icon" />
</Icon>
</el-tooltip>
</div>
<div class="input-suffix-btn" v-if="linkCustom" @click="resetUuid">
<el-tooltip
class="item"
effect="dark"
:content="t('commons.cancel')"
placement="top"
>
<Icon name="icon_close_outlined"><icon_close_outlined class="svg-icon" /></Icon>
</el-tooltip>
</div>
<div class="input-suffix-btn done" v-if="linkCustom" @click="finishEditUuid">
<el-tooltip class="item" effect="dark" :content="t('commons.save')" placement="top">
<Icon name="icon_done_outlined"><icon_done_outlined class="svg-icon" /></Icon>
</el-tooltip>
</div>
</div>
</template>
</el-button>
</el-input>
</div>
<div v-if="shareEnable" class="exp-container share-padding">
<el-checkbox
@@ -72,7 +93,6 @@
<div class="inline-share-item-picker">
<el-date-picker
:clearable="false"
size="small"
class="share-exp-picker"
v-if="state.detailInfo.exp"
v-model="state.detailInfo.exp"
@@ -112,26 +132,40 @@
<div class="inline-share-item" v-if="passwdEnable">
<el-input
ref="pwdRef"
class="link-input-readlonly"
v-model="state.detailInfo.pwd"
:readonly="state.detailInfo.autoPwd"
size="small"
@blur="validatePwdFormat"
>
<template #append>
<div class="share-pwd-opt">
<div
v-if="state.detailInfo.autoPwd"
@click.stop="resetPwd"
class="share-reset-container"
>
<span>{{ t('commons.reset') }}</span>
<template #suffix>
<div class="share-input-suffix">
<span class="suffix-split" />
<div class="input-suffix-btn" @click="copyPwd">
<el-tooltip
class="item"
effect="dark"
:content="t('commons.copy')"
placement="top"
>
<Icon name="de-copy"><deCopy class="svg-icon" /></Icon>
</el-tooltip>
</div>
<div @click.stop="copyPwd" class="share-reset-container">
<span>{{ t('commons.copy') }}</span>
<div class="input-suffix-btn" @click="resetPwd">
<el-tooltip
class="item"
effect="dark"
:content="t('commons.reset')"
placement="top"
>
<Icon name="icon_refresh_outlined"
><icon_refresh_outlined class="svg-icon"
/></Icon>
</el-tooltip>
</div>
</div>
</template>
</el-input>
<el-button secondary @click="openPwdDialog">{{ t('user.change_password') }}</el-button>
</div>
</div>
@@ -149,7 +183,10 @@
<span>{{ t('work_branch.cannot_share_link') }}</span>
</div>
</div>
<div v-if="!shareDisable && shareEnable && showTicket" class="share-ticket-container">
</el-popover>
<custom-link-pwd ref="customPwdRef" @pwd-change="customPwdChange" />
<ticket-dialog ref="ticketDialogRef">
<div v-if="!shareDisable && shareEnable && showTicket">
<share-ticket
:uuid="state.detailInfo.uuid"
:resource-id="props.resourceId"
@@ -158,12 +195,16 @@
@close="closeTicket"
/>
</div>
</el-popover>
</ticket-dialog>
</template>
<script lang="ts" setup>
import icon_shareLabel_outlined from '@/assets/svg/icon_share-label_outlined.svg'
import icon_admin_outlined from '@/assets/svg/icon_admin_outlined.svg'
import icon_edit_outlined from '@/assets/svg/icon_edit_outlined.svg'
import icon_close_outlined from '@/assets/svg/icon_close_outlined.svg'
import icon_done_outlined from '@/assets/svg/icon_done_outlined.svg'
import deCopy from '@/assets/svg/de-copy.svg'
import icon_refresh_outlined from '@/assets/svg/icon_refresh_outlined.svg'
import { useI18n } from '@/hooks/web/useI18n'
import { ref, reactive, computed, nextTick, watch } from 'vue'
import request from '@/config/axios'
@@ -174,6 +215,8 @@ import useClipboard from 'vue-clipboard3'
import ShareTicket from './ShareTicket.vue'
import { useEmbedded } from '@/store/modules/embedded'
import { useShareStoreWithOut } from '@/store/modules/share'
import CustomLinkPwd from './CustomLinkPwd.vue'
import TicketDialog from './TicketDialog.vue'
const shareStore = useShareStoreWithOut()
const embeddedStore = useEmbedded()
const { toClipboard } = useClipboard()
@@ -196,6 +239,9 @@ const expError = ref(false)
const linkCustom = ref(false)
const linkUuidRef = ref(null)
const showTicket = ref(false)
const originUuid = ref('')
const customPwdRef = ref()
const ticketDialogRef = ref()
const state = reactive({
detailInfo: {
id: '',
@@ -223,15 +269,19 @@ const hideShare = async () => {
return
}
}
const pwdValid = validatePwdFormat()
const uuidValid = await validateUuid()
if (pwdValid && uuidValid) {
if (uuidValid) {
popoverVisible.value = false
return
}
}
const clickOutPopover = e => {
if (!popoverVisible.value || e.target.closest('[class*="share-popover"]')) {
if (
!popoverVisible.value ||
e.target.closest('[class*="share-popover"]') ||
e.target.closest('[class*="ed-overlay-dialog"]') ||
e.target.classList?.toString()?.includes('edit-uuid-icon')
) {
return
}
hideShare()
@@ -305,6 +355,7 @@ const loadShareInfo = cb => {
.get({ url })
.then(res => {
state.detailInfo = { ...res.data }
originUuid.value = res.data.uuid
setPageInfo()
})
.finally(() => {
@@ -450,26 +501,6 @@ const validatePwdRequire = () => {
showCheckboxError(t('common.required'), pwdCheckbox)
return false
}
const validatePwdFormat = () => {
if (!shareEnable.value || !passwdEnable.value || state.detailInfo.autoPwd) {
showPageError(null, pwdRef)
return true
}
const val = state.detailInfo.pwd
if (!val) {
showPageError(t('work_branch.password_null_hint'), pwdRef)
return false
}
const regex = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[!@#$%^&*()_+])[A-Za-z\d!@#$%^&*()_+]{4,10}$/
const regep = new RegExp(regex)
if (!regep.test(val)) {
showPageError(t('work_branch.password_hint'), pwdRef)
return false
}
showPageError(null, pwdRef)
resetPwdHandler(val, false)
return true
}
const showCheckboxError = (msg, target, className?: string) => {
if (!target.value) {
return
@@ -597,11 +628,17 @@ const finishEditUuid = async () => {
const uuidValid = await validateUuid()
linkCustom.value = !uuidValid
}
const resetUuid = event => {
event.stopPropagation()
state.detailInfo.uuid = originUuid.value
finishEditUuid()
}
const openTicket = () => {
showTicket.value = true
ticketDialogRef.value.open()
}
const closeTicket = () => {
ticketDialogRef.value.close()
showTicket.value = false
}
const updateRequireTicket = val => {
@@ -611,27 +648,26 @@ const updateRequireTicket = val => {
const execute = () => {
share()
}
const openPwdDialog = () => {
customPwdRef.value.open(state.detailInfo.pwd)
}
const customPwdChange = val => {
state.detailInfo.pwd = val
resetPwdHandler(val, false)
}
defineExpose({
execute
})
</script>
<style lang="less">
.share-popover:not(.share-ticket-popover) {
.share-popover {
padding: 16px 0px !important;
}
.share-ticket-popover {
padding: 0 !important;
}
</style>
<style lang="less" scoped>
.hidden-link-container {
display: none;
}
.share-ticket-container {
padding: 16px;
}
.share-container {
.share-title {
font-weight: 500;
@@ -719,35 +755,17 @@ defineExpose({
}
}
.inline-share-item {
margin-left: 25px;
width: 220px;
display: inline-flex;
column-gap: 12px;
margin-left: 25px;
width: 332px;
:deep(.ed-input-group__append) {
width: initial !important;
background: none;
color: #1f2329;
padding: 0px 0px !important;
.share-pwd-opt {
display: flex;
padding: 1px;
.share-reset-container {
&:not(:first-child) {
border-left: 1px solid var(--ed-input-border-color) !important;
}
width: 45px;
display: flex;
justify-content: center;
&:hover {
cursor: pointer;
background-color: #f5f6f7;
}
&:active {
cursor: pointer;
background-color: #eff0f1;
}
}
}
}
:deep(.link-pwd-error-msg) {
color: red;
@@ -760,4 +778,52 @@ defineExpose({
left: 0px;
}
}
.share-input-suffix {
display: flex;
height: 30px;
line-height: 30px;
column-gap: 4px;
align-items: center;
.suffix-split {
height: 30px;
width: 1px;
display: inline-block;
background-color: #bbbfc4;
margin-right: 4px;
}
.done {
color: #3370ff;
&:hover {
background-color: #3370ff1a !important;
}
}
.input-suffix-btn {
width: 24px;
height: 24px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
&:hover {
background-color: #1f23291a;
}
svg {
width: 16px;
height: 16px;
}
}
}
.link-input-readlonly {
:deep(.ed-input__wrapper) {
background-color: rgba(0, 0, 0, 0.1);
color: #8f959e;
&:hover {
box-shadow: 0 0 0 1px var(--ed-input-border-color, var(--ed-border-color)) inset;
}
input {
color: #646a73;
}
}
}
</style>

View File

@@ -0,0 +1,46 @@
<template>
<div class="ticket-dialog-container">
<el-dialog
v-model="dialogVisible"
:title="t('user.change_password')"
width="600"
class="is-ticket-dialog hidden-footer"
:append-to-body="true"
:before-close="handleClose"
>
<slot />
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
const { t } = useI18n()
const dialogVisible = ref(false)
const handleClose = done => {
done()
}
const open = () => {
dialogVisible.value = true
}
const close = () => {
dialogVisible.value = false
}
defineExpose({
open,
close
})
</script>
<style lang="less" scoped>
.ticket-dialog-container {
width: 100%;
:deep(.is-ticket-dialog) {
.ed-dialog__header {
display: none;
}
}
}
</style>

View File

@@ -0,0 +1,424 @@
<script lang="ts" setup>
import { ref, reactive } from 'vue'
import { ElMessage, ElLoading } from 'element-plus-secondary'
import { useI18n } from '@/hooks/web/useI18n'
import type { FormInstance, FormRules } from 'element-plus-secondary'
import request from '@/config/axios'
import deCopy from '@/assets/svg/de-copy.svg'
import icon_refresh_outlined from '@/assets/svg/icon_refresh_outlined.svg'
import icon_deleteTrash_outlined from '@/assets/svg/icon_delete-trash_outlined.svg'
import icon_add_outlined from '@/assets/svg/icon_add_outlined.svg'
import dvInfo from '@/assets/svg/dv-info.svg'
import useClipboard from 'vue-clipboard3'
import { propTypes } from '@/utils/propTypes'
const { toClipboard } = useClipboard()
const { t } = useI18n()
const dialogVisible = ref(false)
const loadingInstance = ref(null)
const ticketForm = ref<FormInstance>()
const props = defineProps({
uuid: propTypes.string.def('')
})
interface TicketItem {
ticket: string
exp: number
args: string
}
interface TicketArg {
name: string
val: string
}
const isEdit = ref(false)
const linkUrl = ref('')
const state = reactive({
form: reactive<TicketItem>({
ticket: '',
exp: 30,
args: ''
}),
argList: reactive<TicketArg[]>([{ name: '', val: '' }] as TicketArg[])
})
const rule = reactive<FormRules>({
exp: [
{
required: true,
message: t('common.require'),
trigger: 'blur'
}
]
})
const emits = defineEmits(['saved'])
const submitForm = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate(valid => {
if (valid) {
argList2Args()
const param = {
uuid: props.uuid,
ticket: state.form.ticket,
exp: state.form.exp,
args: state.form.args
}
showLoading()
request
.post({ url: '/ticket/saveTicket', data: param })
.then(res => {
if (!res.msg) {
ElMessage.success(t('common.save_success'))
emits('saved')
reset()
}
closeLoading()
})
.catch(() => {
closeLoading()
})
}
})
}
const resetForm = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.resetFields()
dialogVisible.value = false
}
const reset = () => {
resetForm(ticketForm.value)
}
const showLoading = () => {
loadingInstance.value = ElLoading.service({ target: '.ticket-param-drawer' })
}
const closeLoading = () => {
loadingInstance.value?.close()
}
const edit = (row, link) => {
resetFormData()
linkUrl.value = link
isEdit.value = !!row?.ticket
if (!isEdit.value) {
generateTicket()
dialogVisible.value = true
return
}
for (const key in row) {
if (state.form.hasOwnProperty(key)) {
state.form[key] = row[key]
}
}
if (state.form.args) {
args2ArgList()
}
dialogVisible.value = true
}
const generateTicket = () => {
const url = '/ticket/tempTicket'
request.get({ url }).then(res => {
state.form.ticket = res.data
})
}
const addArgRow = () => {
const row: TicketArg = { name: '', val: '' }
state.argList.push(row)
}
const delArgRow = index => {
const last = state.argList.length === 1
if (last) {
state.argList[index]['name'] = ''
state.argList[index]['val'] = ''
return
}
state.argList.splice(index, 1)
}
const args2ArgList = () => {
if (!state.form.args) {
return
}
const argObj = JSON.parse(state.form.args)
for (const key in argObj) {
const val = argObj[key]
if (val && Array.isArray(val)) {
const row = { name: key, val: JSON.stringify(val) }
state.argList.push(row)
} else {
const row = { name: key, val: argObj[key] }
state.argList.push(row)
}
}
if (state.argList?.length > 1 && !state.argList[0]['name']) {
state.argList.splice(0, 1)
}
}
const argList2Args = () => {
if (!state.argList?.length) {
state.form.args = ''
return
}
const argObj = {}
state.argList.forEach(row => {
if (row.name && row.val) {
argObj[row.name] = row.val
}
})
state.form.args = JSON.stringify(argObj)
}
const copyTicket = async ticket => {
const url = `${linkUrl.value}?ticket=${ticket}`
try {
await toClipboard(url)
ElMessage.success(t('common.copy_success'))
} catch (e) {
ElMessage.warning(t('common.copy_unsupported'), e)
}
}
const refreshTicket = () => {
if (!isEdit.value) {
generateTicket()
return
}
const url = '/ticket/saveTicket'
const param = { ticket: state.form.ticket }
param['generateNew'] = true
request.post({ url, data: param }).then(res => {
state.form.ticket = res.data
emits('saved')
})
}
const resetFormData = () => {
state.form = {
ticket: '',
exp: 30,
args: ''
}
state.argList = [{ name: '', val: '' }]
isEdit.value = false
linkUrl.value = ''
}
defineExpose({
edit
})
</script>
<template>
<el-drawer
:title="`${t('commons.add')} Ticket`"
v-model="dialogVisible"
custom-class="ticket-param-drawer"
size="600px"
direction="rtl"
>
<el-form
ref="ticketForm"
require-asterisk-position="right"
:model="state.form"
:rules="rule"
label-width="80px"
label-position="top"
>
<div class="ticket-tips-label"><span>Ticket</span></div>
<div class="ticket-row ticket-tips-label">
<span :title="state.form.ticket">{{ state.form.ticket }}</span>
<el-tooltip class="item" effect="dark" :content="t('commons.copy')" placement="top">
<el-button text @click.stop="copyTicket(state.form.ticket)">
<template #icon>
<Icon name="de-copy"><deCopy class="svg-icon" /></Icon>
</template>
</el-button>
</el-tooltip>
<el-tooltip
class="item"
effect="dark"
:content="`${t('link_ticket.refresh')} ticket`"
placement="top"
>
<el-button text @click.stop="refreshTicket">
<template #icon>
<Icon name="icon_refresh_outlined">
<icon_refresh_outlined class="svg-icon" />
</Icon>
</template>
</el-button>
</el-tooltip>
</div>
<el-form-item prop="exp" :label="t('visualization.over_time')">
<template v-slot:label>
<div class="ticket-form-info-tips">
<span class="custom-form-item__label">{{ t('visualization.over_time') }}</span>
<el-tooltip effect="dark" :content="t('link_ticket.time_tips')" placement="top">
<el-icon>
<Icon name="dv-info"><dvInfo class="svg-icon" /></Icon>
</el-icon>
</el-tooltip>
</div>
</template>
<el-input-number
v-model="state.form.exp"
autocomplete="off"
step-strictly
class="text-left edit-all-line"
:min="0"
:max="1440"
:placeholder="t('common.inputText')"
controls-position="right"
type="number"
/>
</el-form-item>
<el-form-item prop="args" :label="t('dataset.param')">
<template v-slot:label>
<div class="ticket-form-info-tips">
<span class="custom-form-item__label">{{ t('dataset.param') }}</span>
<el-tooltip effect="dark" :content="t('link_ticket.arg_format_tips')" placement="top">
<el-icon>
<Icon name="dv-info"><dvInfo class="svg-icon" /></Icon>
</el-icon>
</el-tooltip>
</div>
</template>
<div class="args-line" v-for="(row, index) in state.argList" :key="index">
<el-input v-model="row['name']" :placeholder="t('visualization.input_param_name')" />
<el-input v-model="row['val']" :placeholder="t('link_ticket.arg_val_tips')" />
<div class="arg-del-btn" @click.stop="delArgRow(index)">
<Icon name="icon_delete-trash_outlined">
<icon_deleteTrash_outlined class="svg-icon" />
</Icon>
</div>
</div>
</el-form-item>
<div class="ticket-add">
<el-button @click.stop="addArgRow" text>
<template #icon>
<icon name="icon_add_outlined"><icon_add_outlined class="svg-icon" /></icon>
</template>
{{ t('commons.add') }}
</el-button>
</div>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button secondary @click="resetForm(ticketForm)">{{ t('common.cancel') }}</el-button>
<el-button type="primary" @click="submitForm(ticketForm)">
{{ t('commons.save') }}
</el-button>
</span>
</template>
</el-drawer>
</template>
<style lang="less">
.ticket-param-drawer {
.ed-drawer__body {
padding: 24px !important;
}
.ed-drawer__footer {
box-shadow: 0 -1px 4px #1f232926 !important;
height: 64px !important;
padding: 16px 24px !important;
.dialog-footer {
height: 32px;
line-height: 32px;
}
}
.ed-form-item__label {
line-height: 22px !important;
height: 22px !important;
.ticket-form-info-tips {
width: fit-content;
display: inline-flex;
align-items: center;
column-gap: 4px;
}
}
.ed-form-item {
&.is-required.asterisk-right {
.ed-form-item__label:after {
display: none;
}
.ticket-form-info-tips {
.custom-form-item__label:after {
content: '*';
color: var(--ed-color-danger);
margin-left: 2px;
font-family: var(--de-custom_font, 'PingFang');
font-size: 14px;
font-style: normal;
font-weight: 400;
}
}
}
}
}
</style>
<style scoped lang="less">
.ticket-param-drawer {
.ed-form-item {
margin-bottom: 16px;
}
.is-error {
margin-bottom: 40px !important;
}
.edit-all-line {
width: 552px !important;
}
.args-line {
width: 100%;
align-items: center;
display: flex;
line-height: 32px;
column-gap: 8px;
.arg-del-btn {
padding: 4px;
width: 24px;
height: 24px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
&:hover {
background-color: #1f23291a;
}
svg {
width: 16px;
height: 16px;
}
}
&:not(:last-child) {
margin-bottom: 8px;
}
}
.ticket-tips-label {
line-height: 22px;
height: 22px;
display: flex;
align-items: center;
margin-bottom: 8px;
}
.ticket-row {
margin-bottom: 16px !important;
span {
margin-right: 8px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
button {
height: 16px;
line-height: 16px;
width: 16px;
}
.ed-button + .ed-button {
margin-left: 8px;
}
}
}
</style>

View File

@@ -1,9 +1,11 @@
package io.dataease.api.xpack.share;
import com.baomidou.mybatisplus.core.metadata.IPage;
import io.dataease.api.xpack.share.request.TicketCreator;
import io.dataease.api.xpack.share.request.TicketDelRequest;
import io.dataease.api.xpack.share.request.TicketSwitchRequest;
import io.dataease.api.xpack.share.vo.TicketVO;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
@@ -13,8 +15,6 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import java.util.List;
@Tag(name = "可视化管理:分享:TICKET")
public interface ShareTicketApi {
@@ -30,8 +30,16 @@ public interface ShareTicketApi {
@Operation(summary = "切换Ticket必填状态")
void switchRequire(@RequestBody TicketSwitchRequest request);
@GetMapping("/query/{resourceId}")
@PostMapping("/pager/{resourceId}/{goPage}/{pageSize}")
@Operation(summary = "根据资源查询Ticket")
@Parameter(name = "resourceId", description = "资源ID", required = true, in = ParameterIn.PATH)
List<TicketVO> query(@PathVariable("resourceId") Long resourceId);
IPage<TicketVO> pager(@PathVariable("resourceId") Long resourceId, @PathVariable("goPage") int goPage, @PathVariable("pageSize") int pageSize);
@GetMapping("/tempTicket")
@Operation(summary = "生成临时Ticket")
String tempTicket();
@GetMapping("/limit")
@Hidden
Integer limit();
}