feat: 查询过滤器保存

页面定时刷新
WebSocket断线自动重连
This commit is contained in:
tk 2024-08-13 11:34:28 +08:00
parent 6922a863ec
commit 779d8e511a
18 changed files with 247 additions and 71 deletions

View File

@ -25,7 +25,6 @@ public record QueryConfigRsp : Sys_Config
public override bool UserRegisterConfirm { get; init; } public override bool UserRegisterConfirm { get; init; }
/// <inheritdoc cref="Sys_Config.UserRegisterDept" /> /// <inheritdoc cref="Sys_Config.UserRegisterDept" />
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public new virtual QueryDeptRsp UserRegisterDept { get; init; } public new virtual QueryDeptRsp UserRegisterDept { get; init; }
/// <inheritdoc cref="Sys_Config.UserRegisterDeptId" /> /// <inheritdoc cref="Sys_Config.UserRegisterDeptId" />
@ -33,7 +32,6 @@ public record QueryConfigRsp : Sys_Config
public override long UserRegisterDeptId { get; init; } public override long UserRegisterDeptId { get; init; }
/// <inheritdoc cref="Sys_Config.UserRegisterRole" /> /// <inheritdoc cref="Sys_Config.UserRegisterRole" />
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public new virtual QueryRoleRsp UserRegisterRole { get; init; } public new virtual QueryRoleRsp UserRegisterRole { get; init; }
/// <inheritdoc cref="Sys_Config.UserRegisterRoleId" /> /// <inheritdoc cref="Sys_Config.UserRegisterRoleId" />

View File

@ -48,7 +48,6 @@ public record QueryLoginLogRsp : Sys_LoginLog
public override string LoginUserName { get; protected init; } public override string LoginUserName { get; protected init; }
/// <inheritdoc cref="Sys_LoginLog.Owner" /> /// <inheritdoc cref="Sys_LoginLog.Owner" />
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public new virtual QueryUserRsp Owner { get; init; } public new virtual QueryUserRsp Owner { get; init; }
/// <inheritdoc cref="Sys_LoginLog.RequestBody" /> /// <inheritdoc cref="Sys_LoginLog.RequestBody" />

View File

@ -17,7 +17,6 @@ public record QueryRequestLogRsp : Sys_RequestLog
public new virtual string CreatedClientIp => base.CreatedClientIp?.ToIpV4(); public new virtual string CreatedClientIp => base.CreatedClientIp?.ToIpV4();
/// <inheritdoc cref="Sys_RequestLog.Api" /> /// <inheritdoc cref="Sys_RequestLog.Api" />
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public new virtual QueryApiRsp Api { get; init; } public new virtual QueryApiRsp Api { get; init; }
/// <inheritdoc cref="Sys_RequestLog.ApiPathCrc32" /> /// <inheritdoc cref="Sys_RequestLog.ApiPathCrc32" />
@ -29,7 +28,6 @@ public record QueryRequestLogRsp : Sys_RequestLog
public override DateTime CreatedTime { get; init; } public override DateTime CreatedTime { get; init; }
/// <inheritdoc cref="Sys_RequestLog.Detail" /> /// <inheritdoc cref="Sys_RequestLog.Detail" />
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public new virtual QueryRequestLogDetailRsp Detail { get; init; } public new virtual QueryRequestLogDetailRsp Detail { get; init; }
/// <inheritdoc cref="Sys_RequestLog.Duration" /> /// <inheritdoc cref="Sys_RequestLog.Duration" />
@ -45,7 +43,6 @@ public record QueryRequestLogRsp : Sys_RequestLog
public override int HttpStatusCode { get; init; } public override int HttpStatusCode { get; init; }
/// <inheritdoc cref="Sys_RequestLog.Owner" /> /// <inheritdoc cref="Sys_RequestLog.Owner" />
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public new virtual QueryUserRsp Owner { get; init; } public new virtual QueryUserRsp Owner { get; init; }
/// <inheritdoc cref="IFieldOwner.OwnerId" /> /// <inheritdoc cref="IFieldOwner.OwnerId" />

View File

@ -24,7 +24,6 @@ public record QuerySiteMsgRsp : Sys_SiteMsg
public override string CreatedUserName { get; init; } public override string CreatedUserName { get; init; }
/// <inheritdoc cref="Sys_SiteMsg.Depts" /> /// <inheritdoc cref="Sys_SiteMsg.Depts" />
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public new virtual IEnumerable<QueryDeptRsp> Depts { get; init; } public new virtual IEnumerable<QueryDeptRsp> Depts { get; init; }
/// <inheritdoc cref="EntityBase{T}.Id" /> /// <inheritdoc cref="EntityBase{T}.Id" />
@ -45,7 +44,6 @@ public record QuerySiteMsgRsp : Sys_SiteMsg
public QuerySiteMsgFlagRsp MyFlags { get; init; } public QuerySiteMsgFlagRsp MyFlags { get; init; }
/// <inheritdoc cref="Sys_SiteMsg.Roles" /> /// <inheritdoc cref="Sys_SiteMsg.Roles" />
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public new virtual IEnumerable<QueryRoleRsp> Roles { get; init; } public new virtual IEnumerable<QueryRoleRsp> Roles { get; init; }
/// <summary> /// <summary>
@ -62,7 +60,6 @@ public record QuerySiteMsgRsp : Sys_SiteMsg
public override string Title { get; init; } public override string Title { get; init; }
/// <inheritdoc cref="Sys_SiteMsg.Users" /> /// <inheritdoc cref="Sys_SiteMsg.Users" />
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public new virtual IEnumerable<QueryUserRsp> Users { get; init; } public new virtual IEnumerable<QueryUserRsp> Users { get; init; }
/// <inheritdoc cref="IFieldVersion.Version" /> /// <inheritdoc cref="IFieldVersion.Version" />

View File

@ -207,7 +207,9 @@ public sealed class UserService(
.ConfigureAwait(false); .ConfigureAwait(false);
} }
return dbUser == null ? throw new NetAdminInvalidOperationException(Ln.) : LoginInternal(dbUser); return dbUser == null
? throw new NetAdminInvalidOperationException(Ln.)
: await LoginInternalAsync(dbUser).ConfigureAwait(false);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -221,7 +223,9 @@ public sealed class UserService(
} }
var dbUser = await Rpo.Where(a => a.Mobile == req.DestDevice).ToOneAsync().ConfigureAwait(false); var dbUser = await Rpo.Where(a => a.Mobile == req.DestDevice).ToOneAsync().ConfigureAwait(false);
return dbUser == null ? throw new NetAdminInvalidOperationException(Ln.) : LoginInternal(dbUser); return dbUser == null
? throw new NetAdminInvalidOperationException(Ln.)
: await LoginInternalAsync(dbUser).ConfigureAwait(false);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -229,7 +233,7 @@ public sealed class UserService(
{ {
var dbUser = await Rpo.Where(a => a.Id == userId).ToOneAsync().ConfigureAwait(false); var dbUser = await Rpo.Where(a => a.Id == userId).ToOneAsync().ConfigureAwait(false);
return LoginInternal(dbUser); return await LoginInternalAsync(dbUser).ConfigureAwait(false);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -464,14 +468,15 @@ public sealed class UserService(
return dept.Count != 1 ? throw new NetAdminInvalidOperationException(Ln.) : roles; return dept.Count != 1 ? throw new NetAdminInvalidOperationException(Ln.) : roles;
} }
private LoginRsp LoginInternal(Sys_User dbUser) private async Task<LoginRsp> LoginInternalAsync(Sys_User dbUser)
{ {
if (!dbUser.Enabled) { if (!dbUser.Enabled) {
throw new NetAdminInvalidOperationException(Ln.); throw new NetAdminInvalidOperationException(Ln.);
} }
_ = UpdateAsync(dbUser with { LastLoginTime = DateTime.Now }, [nameof(Sys_User.LastLoginTime)] _ = await UpdateAsync(dbUser with { LastLoginTime = DateTime.Now }, [nameof(Sys_User.LastLoginTime)]
, ignoreVersion: true); , ignoreVersion: true)
.ConfigureAwait(false);
var tokenPayload var tokenPayload
= new Dictionary<string, object> { { nameof(ContextUserToken), dbUser.Adapt<ContextUserToken>() } }; = new Dictionary<string, object> { { nameof(ContextUserToken), dbUser.Adapt<ContextUserToken>() } };

View File

@ -76,15 +76,59 @@
<el-badge :hidden="vue.query.dynamicFilter.filters.length === 0" :value="vue.query.dynamicFilter.filters.length"> <el-badge :hidden="vue.query.dynamicFilter.filters.length === 0" :value="vue.query.dynamicFilter.filters.length">
<el-button-group> <el-button-group>
<el-button @click="search" icon="el-icon-search" type="primary">{{ $t('查询') }}</el-button> <el-button @click="search" icon="el-icon-search" type="primary">{{ $t('查询') }}</el-button>
<el-popover :title="$t('已应用的查询条件')" placement="bottom-end" trigger="hover" width="40rem"> <el-popover :title="$t('已应用的查询条件')" :width="popWidth" placement="bottom-end" trigger="hover">
<template #reference> <template #reference>
<el-button @click="reset" icon="el-icon-refresh-left">{{ $t('重置') }}</el-button> <el-button @click="reset" icon="el-icon-refresh-left">{{ $t('重置') }}</el-button>
</template> </template>
<v-ace-editor <v-ace-editor
:theme="this.$TOOL.data.get('APP_SET_DARK') || this.$CONFIG.APP_SET_DARK ? 'github_dark' : 'github'" v-model:value="aceEditorValue"
:value="vkbeautify().json(vue.query, 2)" :theme="$TOOL.data.get('APP_SET_DARK') || $CONFIG.APP_SET_DARK ? 'github_dark' : 'github'"
lang="json" lang="json"
style="height: 20rem; width: 100%" /> style="height: 20rem; width: 100%" />
<p class="mt-4 flex gap05 items-center" style="justify-content: right">
<span class="text-right" style="width: 5rem">{{ $t('全局') }}</span>
<el-select v-model="this.aceEditorValue" :key="selectFilterKey" :teleported="false" style="flex-grow: 1">
<el-option
v-for="(item, i) in $TOOL.data.get('APP_SET_QUERY_FILTERS') || []"
:key="i"
:label="item.name"
:value="item.value" />
</el-select>
<el-button-group>
<el-button @click="saveFilter(true)" plain type="primary">{{ $t('保存') }}</el-button>
<el-button @click="delFilter(true)" plain>{{ $t('删除') }}</el-button>
</el-button-group>
</p>
<p class="mt-4 flex gap05 items-center" style="justify-content: right">
<span class="text-right" style="width: 5rem">{{ $t('本页') }}</span>
<el-select v-model="this.aceEditorValue" :key="selectFilterKey" :teleported="false" style="flex-grow: 1">
<el-option
v-for="(item, i) in $TOOL.data.get('APP_SET_QUERY_FILTERS_' + this.queryApi) || []"
:key="i"
:label="item.name"
:value="item.value" />
</el-select>
<el-button-group>
<el-button @click="saveFilter(false)" plain type="primary">{{ $t('保存') }}</el-button>
<el-button @click="delFilter(false)" plain>{{ $t('删除') }}</el-button>
</el-button-group>
</p>
<p class="mt-4 text-right">
<el-button @click="jsonFormat">{{ $t('JSON 格式化') }}</el-button>
<el-dropdown :teleported="false">
<el-button @click="reSearch" type="primary">{{ $t('重新查询') }}</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="reSearch(5)">5s</el-dropdown-item>
<el-dropdown-item @click="reSearch(10)">10s</el-dropdown-item>
<el-dropdown-item @click="reSearch(15)">15s</el-dropdown-item>
<el-dropdown-item @click="reSearch(30)">30s</el-dropdown-item>
<el-dropdown-item @click="reSearch(60)">60s</el-dropdown-item>
<el-dropdown-item @click="reSearch(120)">120s</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</p>
</el-popover> </el-popover>
</el-button-group> </el-button-group>
</el-badge> </el-badge>
@ -96,7 +140,7 @@ import tool from '@/utils/tool'
import vkbeautify from 'vkbeautify/index' import vkbeautify from 'vkbeautify/index'
export default { export default {
emits: ['search', 'reset'], emits: ['search', 'reset', 'reSearch'],
props: { props: {
dateField: { type: String, default: 'createdTime' }, dateField: { type: String, default: 'createdTime' },
hasDate: { type: Boolean, default: true }, hasDate: { type: Boolean, default: true },
@ -108,6 +152,11 @@ export default {
}, },
data() { data() {
return { return {
autoResearchTimer: null,
queryApi: null,
popWidth: '40rem',
selectFilterKey: 0,
aceEditorValue: null,
selectInputKey: null, selectInputKey: null,
dateShortCuts: [ dateShortCuts: [
{ {
@ -356,8 +405,23 @@ export default {
}, },
} }
}, },
mounted() {}, mounted() {
this.queryApi = this.vue.$refs.table.queryApi.url.toUpperCase()
},
watch: {
'vue.query': {
immediate: true,
deep: true,
handler: function (o, n) {
this.aceEditorValue = this.vkbeautify.json(n, 2)
},
},
},
async created() { async created() {
if (document.body.clientWidth < 1000) {
this.popWidth = '100%'
}
this.aceEditorValue = this.vkbeautify.json(this.vue.query, 2)
this.selectInputKey = this.controls.find((x) => x.type === 'select-input')?.field[1][0].key this.selectInputKey = this.controls.find((x) => x.type === 'select-input')?.field[1][0].key
if (this.dateType === 'datetimerange') { if (this.dateType === 'datetimerange') {
this.dateShortCuts.unshift( this.dateShortCuts.unshift(
@ -405,8 +469,70 @@ export default {
tool() { tool() {
return tool return tool
}, },
vkbeautify() {
return vkbeautify
},
}, },
methods: { methods: {
jsonFormat() {
try {
this.aceEditorValue = vkbeautify.json(this.aceEditorValue, 2)
} catch {
this.$message.error(this.$t('格式错误'))
}
},
async reSearch(sec) {
const newParam = JSON.parse(this.aceEditorValue)
this.vue.$refs.table.tableParams = newParam
this.vue.$refs.table.upData()
await this.$nextTick()
this.vue.$refs.table.tableParams = this.vue.query
this.$emit('reSearch', newParam)
if (typeof sec !== 'number') return
const timerEl = document.getElementsByClassName('autoResearchTimer')[0]
if (!timerEl) {
this.$message({
showClose: true,
onClose: () => clearInterval(this.autoResearchTimer),
type: 'warning',
customClass: 'autoResearchTimer',
message: this.$t('{s} 秒后刷新...', { s: sec }),
duration: 0,
})
this.autoResearchTimer = setInterval(() => {
const el = document.getElementsByClassName('autoResearchTimer')[0].getElementsByClassName('el-message__content')[0]
let num = parseInt(/(\d+)/.exec(el.innerHTML)[0])
if (num === 1) {
this.reSearch()
num = sec + 1
}
el.innerHTML = el.innerHTML.replace(/\d+/, (num - 1).toString())
}, 1000)
}
},
async delFilter(isGlobal) {
const key = isGlobal ? 'APP_SET_QUERY_FILTERS' : 'APP_SET_QUERY_FILTERS_' + this.queryApi
let filters = this.$TOOL.data.get(key) || []
filters = filters.filter((x) => x.value !== this.aceEditorValue)
await this.$TOOL.data.set(key, filters)
this.$message.success(this.$t('删除成功'))
this.selectFilterKey = Math.random()
},
async saveFilter(isGlobal) {
const key = isGlobal ? 'APP_SET_QUERY_FILTERS' : 'APP_SET_QUERY_FILTERS_' + this.queryApi
try {
const filterName = await this.$prompt('设置一个过滤器名称', '保存查询条件', {
inputPattern: /\S/,
inputErrorMessage: '名称不能为空',
})
let filters = this.$TOOL.data.get(key) || []
filters = filters.filter((x) => x.name !== filterName.value)
filters.push({ name: filterName.value, value: this.aceEditorValue })
await this.$TOOL.data.set(key, filters)
this.$message.success(this.$t('保存成功'))
} catch {}
},
trimSpaces(key) { trimSpaces(key) {
this.form[key][this.selectInputKey] = this.form[key][this.selectInputKey].replace(/^\s*(.*?)\s*$/g, '$1') this.form[key][this.selectInputKey] = this.form[key][this.selectInputKey].replace(/^\s*(.*?)\s*$/g, '$1')
}, },
@ -415,9 +541,6 @@ export default {
delete this.form[item.field[0]][field.key] delete this.form[item.field[0]][field.key]
} }
}, },
vkbeautify() {
return vkbeautify
},
search() { search() {
const parentQuery = this.clearParentQuery() const parentQuery = this.clearParentQuery()
Object.assign(parentQuery, this.form.root || {}) Object.assign(parentQuery, this.form.root || {})

View File

@ -6,24 +6,36 @@ import config from '@/config'
export default { export default {
data() { data() {
return {} return {
ws: null,
}
}, },
async created() { async created() {
const ws = new WebSocket(`ws://${config.API_URL.replace('http://', '')}/ws/version`) this.connectWebSocket()
ws.onopen = () => { },
ws.send('1') methods: {
connectWebSocket() {
this.ws = new WebSocket(
import.meta.env.MODE === 'production'
? `wss://${config.API_URL.replace('https://', '')}/ws/version`
: `ws://${window.location.host}/ws/version`,
)
this.ws.onopen = () => {
this.ws.send('1')
} }
ws.onmessage = async (res) => { this.ws.onmessage = async (res) => {
if (res.data !== this.$TOOL.data.get('APP_VERSION')) { if (res.data !== this.$TOOL.data.get('APP_VERSION')) {
await this.$TOOL.data.set('APP_VERSION', res.data) await this.$TOOL.data.set('APP_VERSION', res.data)
this.showTip(res.data.slice(0, res.data.indexOf('+'))) this.showTip(res.data.slice(0, res.data.indexOf('+')))
} else { } else {
await new Promise((x) => setTimeout(x, 10000)) await new Promise((x) => setTimeout(x, 10000))
ws.send('1') this.ws.send('1')
} }
} }
this.ws.onclose = () => {
setTimeout(this.connectWebSocket, 5000) // 5
}
}, },
methods: {
/** /**
* 通知消息 * 通知消息
*/ */

View File

@ -9,7 +9,7 @@
<template> <template>
<div class="sc-dialog" ref="scDialog"> <div class="sc-dialog" ref="scDialog">
<el-dialog v-bind="$attrs" v-model="dialogVisible" :fullscreen="isFullscreen" :show-close="false" ref="dialog"> <el-dialog v-bind="$attrs" v-model="dialogVisible" :fullscreen="isFullscreen" :show-close="false" draggable ref="dialog">
<template #header> <template #header>
<slot name="header"> <slot name="header">
<span class="el-dialog__title">{{ title }}</span> <span class="el-dialog__title">{{ title }}</span>

View File

@ -24,6 +24,12 @@
</el-icon> </el-icon>
刷新 刷新
</li> </li>
<li @click="scheduledRefresh()">
<el-icon>
<el-icon-alarm-clock />
</el-icon>
定时刷新
</li>
<hr /> <hr />
<li :class="contextMenuItem.meta.affix ? 'disabled' : ''" @click="closeTabs()"> <li :class="contextMenuItem.meta.affix ? 'disabled' : ''" @click="closeTabs()">
<el-icon> <el-icon>
@ -61,6 +67,7 @@ export default {
name: 'tags', name: 'tags',
data() { data() {
return { return {
refreshTimer: null,
contextMenuVisible: false, contextMenuVisible: false,
contextMenuItem: null, contextMenuItem: null,
left: 0, left: 0,
@ -69,7 +76,9 @@ export default {
tipDisplayed: false, tipDisplayed: false,
} }
}, },
props: {}, props: {
vue: { type: Object },
},
watch: { watch: {
$route(e) { $route(e) {
this.addViewTags(e) this.addViewTags(e)
@ -188,27 +197,38 @@ export default {
this.contextMenuItem = null this.contextMenuItem = null
this.contextMenuVisible = false this.contextMenuVisible = false
}, },
async scheduledRefresh() {
this.closeMenu()
try {
const sleep = await this.$prompt('刷新时间间隔(秒)', '定时刷新', {
inputPattern: /^[1-9]\d*$/,
inputErrorMessage: '时间必须为数字',
inputValue: '10',
})
const sleepSecs = parseInt(sleep.value)
this.$message({
showClose: true,
onClose: () => clearInterval(this.refreshTimer),
type: 'warning',
customClass: 'refreshTimer',
message: this.$t('{s} 秒后刷新...', { s: sleepSecs }),
duration: 0,
})
this.refreshTimer = setInterval(() => {
const el = document.getElementsByClassName('refreshTimer')[0].getElementsByClassName('el-message__content')[0]
let num = parseInt(/(\d+)/.exec(el.innerHTML)[0])
if (num === 1) {
this.vue.routerViewKey = Math.random()
num = sleepSecs + 1
}
el.innerHTML = el.innerHTML.replace(/\d+/, (num - 1).toString())
}, 1000)
} catch {}
},
//TAB //TAB
refreshTab() { refreshTab() {
this.contextMenuVisible = false this.closeMenu()
const nowTag = this.contextMenuItem this.vue.routerViewKey = Math.random()
//
if (this.$route.fullPath !== nowTag.fullPath) {
this.$router.push({
path: nowTag.fullPath,
query: nowTag.query,
})
}
this.$store.commit('refreshIframe', nowTag)
setTimeout(() => {
this.$store.commit('removeKeepLive', nowTag.name)
this.$store.commit('setRouteShow', false)
this.$nextTick(() => {
this.$store.commit('pushKeepLive', nowTag.name)
this.$store.commit('setRouteShow', true)
})
}, 0)
}, },
//TAB //TAB
closeTabs() { closeTabs() {

View File

@ -46,9 +46,9 @@
</div> </div>
<Side-m v-if="ismobile"></Side-m> <Side-m v-if="ismobile"></Side-m>
<div class="aminui-body el-container"> <div class="aminui-body el-container">
<Tags v-if="!ismobile && layoutTags"></Tags> <Tags v-if="!ismobile && layoutTags" :vue="this"></Tags>
<div class="adminui-main" id="adminui-main"> <div class="adminui-main" id="adminui-main">
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }" :key="routerViewKey">
<keep-alive> <keep-alive>
<component v-if="$store.state.keepAlive.routeShow" :is="Component" :key="$route.fullPath" /> <component v-if="$store.state.keepAlive.routeShow" :is="Component" :key="$route.fullPath" />
</keep-alive> </keep-alive>
@ -93,9 +93,9 @@
</div> </div>
<Side-m v-if="ismobile"></Side-m> <Side-m v-if="ismobile"></Side-m>
<div class="aminui-body el-container"> <div class="aminui-body el-container">
<Tags v-if="!ismobile && layoutTags"></Tags> <Tags v-if="!ismobile && layoutTags" :vue="this"></Tags>
<div class="adminui-main" id="adminui-main"> <div class="adminui-main" id="adminui-main">
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }" :key="routerViewKey">
<keep-alive> <keep-alive>
<component v-if="$store.state.keepAlive.routeShow" :is="Component" :key="$route.fullPath" /> <component v-if="$store.state.keepAlive.routeShow" :is="Component" :key="$route.fullPath" />
</keep-alive> </keep-alive>
@ -135,9 +135,9 @@
</header> </header>
<section class="aminui-wrapper"> <section class="aminui-wrapper">
<div class="aminui-body el-container"> <div class="aminui-body el-container">
<Tags v-if="!ismobile && layoutTags"></Tags> <Tags v-if="!ismobile && layoutTags" :vue="this"></Tags>
<div class="adminui-main" id="adminui-main"> <div class="adminui-main" id="adminui-main">
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }" :key="routerViewKey">
<keep-alive> <keep-alive>
<component v-if="$store.state.keepAlive.routeShow" :is="Component" :key="$route.fullPath" /> <component v-if="$store.state.keepAlive.routeShow" :is="Component" :key="$route.fullPath" />
</keep-alive> </keep-alive>
@ -195,9 +195,9 @@
<Topbar> <Topbar>
<userbar></userbar> <userbar></userbar>
</Topbar> </Topbar>
<Tags v-if="!ismobile && layoutTags"></Tags> <Tags v-if="!ismobile && layoutTags" :vue="this"></Tags>
<div class="adminui-main" id="adminui-main"> <div class="adminui-main" id="adminui-main">
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }" :key="routerViewKey">
<keep-alive> <keep-alive>
<component v-if="$store.state.keepAlive.routeShow" :is="Component" :key="$route.fullPath" /> <component v-if="$store.state.keepAlive.routeShow" :is="Component" :key="$route.fullPath" />
</keep-alive> </keep-alive>
@ -239,6 +239,7 @@ export default {
}, },
data() { data() {
return { return {
routerViewKey: 0,
menu: [], menu: [],
nextMenu: [], nextMenu: [],
pmenu: {}, pmenu: {},

View File

@ -436,7 +436,6 @@ export default {
周四: 'Thursday', 周四: 'Thursday',
周五: 'Friday', 周五: 'Friday',
周六: 'Saturday', 周六: 'Saturday',
JSON格式化: 'JSON formatting',
确定: 'Confirm', 确定: 'Confirm',
无权限或找不到页面: 'No permission or page not found', 无权限或找不到页面: 'No permission or page not found',
'当前页面无权限访问或者打开了一个不存在的链接,请检查当前账户权限和链接的可访问性。': '当前页面无权限访问或者打开了一个不存在的链接,请检查当前账户权限和链接的可访问性。':
@ -467,6 +466,7 @@ export default {
用户列表: 'User list', 用户列表: 'User list',
: 'Yes', : 'Yes',
: 'No', : 'No',
资源复用: 'Resource reuse',
手机: 'Mobile', 手机: 'Mobile',
'您已退出登录或无权限访问当前资源,请重新登录后再操作': '您已退出登录或无权限访问当前资源,请重新登录后再操作':
'You have logged out or do not have permission to access the current resource, please log in again before operating', 'You have logged out or do not have permission to access the current resource, please log in again before operating',
@ -497,4 +497,10 @@ export default {
请输入操作符: 'Please enter operator', 请输入操作符: 'Please enter operator',
查询字段: 'Query field', 查询字段: 'Query field',
最后登录: 'Last login', 最后登录: 'Last login',
'{s} 秒后刷新...': '{s} seconds to refresh...',
重新查询: 'Re-query',
全局: 'Global',
本页: 'Page',
'JSON 格式化': 'JSON formatting',
格式错误: 'Format error',
} }

View File

@ -435,7 +435,6 @@ export default {
周四: '周四', 周四: '周四',
周五: '周五', 周五: '周五',
周六: '周六', 周六: '周六',
JSON格式化: 'JSON格式化',
确定: '确定', 确定: '确定',
无权限或找不到页面: '无权限或找不到页面', 无权限或找不到页面: '无权限或找不到页面',
'当前页面无权限访问或者打开了一个不存在的链接,请检查当前账户权限和链接的可访问性。': '当前页面无权限访问或者打开了一个不存在的链接,请检查当前账户权限和链接的可访问性。':
@ -465,6 +464,7 @@ export default {
用户列表: '用户列表', 用户列表: '用户列表',
: '是', : '是',
: '否', : '否',
资源复用: '资源复用',
手机: '手机', 手机: '手机',
'您已退出登录或无权限访问当前资源,请重新登录后再操作': '您已退出登录或无权限访问当前资源,请重新登录后再操作', '您已退出登录或无权限访问当前资源,请重新登录后再操作': '您已退出登录或无权限访问当前资源,请重新登录后再操作',
访问受限: '访问受限', 访问受限: '访问受限',
@ -494,4 +494,10 @@ export default {
请输入操作符: '请输入操作符', 请输入操作符: '请输入操作符',
查询字段: '查询字段', 查询字段: '查询字段',
最后登录: '最后登录', 最后登录: '最后登录',
'{s} 秒后刷新...': '{s} 秒后刷新...',
重新查询: '重新查询',
全局: '全局',
本页: '本页',
'JSON 格式化': 'JSON 格式化',
格式错误: '格式错误',
} }

View File

@ -513,6 +513,10 @@ textarea {
text-align: center; text-align: center;
} }
.text-right {
text-align: right;
}
.flex { .flex {
display: flex; display: flex;
} }

View File

@ -93,6 +93,7 @@
'nextExecTime', 'nextExecTime',
'enabled', 'enabled',
'createdTime', 'createdTime',
'lastDuration',
]" ]"
:default-sort="{ prop: 'lastExecTime', order: 'descending' }" :default-sort="{ prop: 'lastExecTime', order: 'descending' }"
:export-api="$API.sys_job.export" :export-api="$API.sys_job.export"

View File

@ -80,7 +80,9 @@
:theme="this.$TOOL.data.get('APP_SET_DARK') || this.$CONFIG.APP_SET_DARK ? 'github_dark' : 'github'" :theme="this.$TOOL.data.get('APP_SET_DARK') || this.$CONFIG.APP_SET_DARK ? 'github_dark' : 'github'"
lang="json" lang="json"
style="height: 30rem; width: 100%" /> style="height: 30rem; width: 100%" />
<el-button @click="form.dashboardLayout = jsonFormat(form.dashboardLayout)" type="text">{{ $t('JSON格式化') }}</el-button> <el-button @click="form.dashboardLayout = jsonFormat(form.dashboardLayout)" type="text">{{
$t('JSON 格式化')
}}</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</el-tab-pane> </el-tab-pane>

View File

@ -69,7 +69,7 @@
</el-header> </el-header>
<el-main class="nopadding"> <el-main class="nopadding">
<sc-table <sc-table
:context-menus="['id', 'userName', 'mobile', 'email', 'enabled', 'createdTime']" :context-menus="['id', 'userName', 'mobile', 'email', 'enabled', 'createdTime', 'lastLoginTime']"
:context-opers="['view', 'edit']" :context-opers="['view', 'edit']"
:default-sort="{ prop: 'createdTime', order: 'descending' }" :default-sort="{ prop: 'createdTime', order: 'descending' }"
:export-api="$API.sys_user.export" :export-api="$API.sys_user.export"

View File

@ -26,6 +26,11 @@ export default defineConfig({
changeOrigin: true, changeOrigin: true,
rewrite: (p) => p.replace(/^\/api/, ''), rewrite: (p) => p.replace(/^\/api/, ''),
}, },
'/ws': {
target: 'ws://localhost:5010/ws',
ws: true,
rewrite: (p) => p.replace(/^\/ws/, ''),
},
}, },
}, },
css: { css: {