Merge pull request #171 from nsnail/tk

feat:  查询过滤器保存
This commit is contained in:
nsnail 2024-08-13 11:34:49 +08:00 committed by GitHub
commit b9b228c9e1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 247 additions and 71 deletions

View File

@ -11,4 +11,4 @@
"path": "node_modules/cz-git"
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -207,7 +207,9 @@ public sealed class UserService(
.ConfigureAwait(false);
}
return dbUser == null ? throw new NetAdminInvalidOperationException(Ln.) : LoginInternal(dbUser);
return dbUser == null
? throw new NetAdminInvalidOperationException(Ln.)
: await LoginInternalAsync(dbUser).ConfigureAwait(false);
}
/// <inheritdoc />
@ -221,7 +223,9 @@ public sealed class UserService(
}
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 />
@ -229,7 +233,7 @@ public sealed class UserService(
{
var dbUser = await Rpo.Where(a => a.Id == userId).ToOneAsync().ConfigureAwait(false);
return LoginInternal(dbUser);
return await LoginInternalAsync(dbUser).ConfigureAwait(false);
}
/// <inheritdoc />
@ -464,14 +468,15 @@ public sealed class UserService(
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) {
throw new NetAdminInvalidOperationException(Ln.);
}
_ = UpdateAsync(dbUser with { LastLoginTime = DateTime.Now }, [nameof(Sys_User.LastLoginTime)]
, ignoreVersion: true);
_ = await UpdateAsync(dbUser with { LastLoginTime = DateTime.Now }, [nameof(Sys_User.LastLoginTime)]
, ignoreVersion: true)
.ConfigureAwait(false);
var tokenPayload
= 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-button-group>
<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>
<el-button @click="reset" icon="el-icon-refresh-left">{{ $t('重置') }}</el-button>
</template>
<v-ace-editor
:theme="this.$TOOL.data.get('APP_SET_DARK') || this.$CONFIG.APP_SET_DARK ? 'github_dark' : 'github'"
:value="vkbeautify().json(vue.query, 2)"
v-model:value="aceEditorValue"
:theme="$TOOL.data.get('APP_SET_DARK') || $CONFIG.APP_SET_DARK ? 'github_dark' : 'github'"
lang="json"
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-button-group>
</el-badge>
@ -96,7 +140,7 @@ import tool from '@/utils/tool'
import vkbeautify from 'vkbeautify/index'
export default {
emits: ['search', 'reset'],
emits: ['search', 'reset', 'reSearch'],
props: {
dateField: { type: String, default: 'createdTime' },
hasDate: { type: Boolean, default: true },
@ -108,6 +152,11 @@ export default {
},
data() {
return {
autoResearchTimer: null,
queryApi: null,
popWidth: '40rem',
selectFilterKey: 0,
aceEditorValue: null,
selectInputKey: null,
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() {
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
if (this.dateType === 'datetimerange') {
this.dateShortCuts.unshift(
@ -405,8 +469,70 @@ export default {
tool() {
return tool
},
vkbeautify() {
return vkbeautify
},
},
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) {
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]
}
},
vkbeautify() {
return vkbeautify
},
search() {
const parentQuery = this.clearParentQuery()
Object.assign(parentQuery, this.form.root || {})

View File

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

View File

@ -9,7 +9,7 @@
<template>
<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>
<slot name="header">
<span class="el-dialog__title">{{ title }}</span>

View File

@ -24,6 +24,12 @@
</el-icon>
刷新
</li>
<li @click="scheduledRefresh()">
<el-icon>
<el-icon-alarm-clock />
</el-icon>
定时刷新
</li>
<hr />
<li :class="contextMenuItem.meta.affix ? 'disabled' : ''" @click="closeTabs()">
<el-icon>
@ -61,6 +67,7 @@ export default {
name: 'tags',
data() {
return {
refreshTimer: null,
contextMenuVisible: false,
contextMenuItem: null,
left: 0,
@ -69,7 +76,9 @@ export default {
tipDisplayed: false,
}
},
props: {},
props: {
vue: { type: Object },
},
watch: {
$route(e) {
this.addViewTags(e)
@ -188,27 +197,38 @@ export default {
this.contextMenuItem = null
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
refreshTab() {
this.contextMenuVisible = false
const nowTag = this.contextMenuItem
//
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)
this.closeMenu()
this.vue.routerViewKey = Math.random()
},
//TAB
closeTabs() {

View File

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

View File

@ -436,7 +436,6 @@ export default {
周四: 'Thursday',
周五: 'Friday',
周六: 'Saturday',
JSON格式化: 'JSON formatting',
确定: 'Confirm',
无权限或找不到页面: 'No permission or page not found',
'当前页面无权限访问或者打开了一个不存在的链接,请检查当前账户权限和链接的可访问性。':
@ -467,6 +466,7 @@ export default {
用户列表: 'User list',
: 'Yes',
: 'No',
资源复用: 'Resource reuse',
手机: 'Mobile',
'您已退出登录或无权限访问当前资源,请重新登录后再操作':
'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',
查询字段: 'Query field',
最后登录: '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-right {
text-align: right;
}
.flex {
display: flex;
}

View File

@ -93,6 +93,7 @@
'nextExecTime',
'enabled',
'createdTime',
'lastDuration',
]"
:default-sort="{ prop: 'lastExecTime', order: 'descending' }"
: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'"
lang="json"
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>
</el-tab-pane>

View File

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

View File

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