feat: 插件管理 消息管理 优化

This commit is contained in:
dataeaseShu 2022-08-23 17:42:43 +08:00
parent a0b92c91ec
commit 1403504e2a
8 changed files with 786 additions and 850 deletions

View File

@ -34,7 +34,7 @@ export default {
return backPath || backName || backTo return backPath || backName || backTo
}, },
needInnerPadding() { needInnerPadding() {
return ['sys-task-email', 'system-dept', 'system-dept-form', 'system-auth', 'sys-appearance', 'system-param', 'system-template', "sys-task-dataset"].includes(this.$route.name) return ['sys-task-email', 'system-dept', 'system-dept-form', 'system-auth', 'sys-appearance', 'system-param', 'system-template', "sys-task-dataset", "sys-msg-web-all", "system-plugin"].includes(this.$route.name)
} }
} }
} }

View File

@ -25,6 +25,7 @@
</template> </template>
<script> <script>
import { log } from '@antv/g2plot/lib/utils';
import tableBody from "./tableBody"; import tableBody from "./tableBody";
export default { export default {
components: { tableBody }, components: { tableBody },

View File

@ -2169,7 +2169,7 @@ export default {
i18n_msg_type_ds_invalid: '数据源失效', i18n_msg_type_ds_invalid: '数据源失效',
i18n_msg_type_all: '全部类型', i18n_msg_type_all: '全部类型',
channel_inner_msg: '站内消息', channel_inner_msg: '站内消息',
channel_email_msg: '邮件' channel_email_msg: '邮件提醒'
}, },
denumberrange: { denumberrange: {
label: '数值区间', label: '数值区间',

View File

@ -1,191 +1,328 @@
<template> <template>
<layout-content v-loading="$store.getters.loadingMap[$store.getters.currentPath]"> <de-layout-content
:header="$t('消息列表')"
v-loading="$store.getters.loadingMap[$store.getters.currentPath]"
>
<div class="organization">
<el-tabs v-model="tabActive" @tab-click="changeTab">
<el-tab-pane :label="$t('未读消息')" name="unread"> </el-tab-pane>
<el-tab-pane :label="$t('已读消息')" name="readed"> </el-tab-pane>
<el-tab-pane :label="$t('全部消息')" name="allMsg"> </el-tab-pane>
</el-tabs>
<div class="tabs-container">
<div class="msg-cont">
<el-row class="top-operate">
<el-col :span="12">
<template v-if="tabActive === 'unread'">
<deBtn secondary @click="allMarkReaded">{{
$t("webmsg.all_mark_readed")
}}</deBtn>
<deBtn
secondary
:disabled="multipleSelection.length === 0"
@click="markReaded"
>{{ $t("webmsg.mark_readed") }}</deBtn
>
</template>
<deBtn
v-if="tabActive === 'readed'"
secondary
:disabled="multipleSelection.length === 0"
@click="deleteBatch"
>{{ $t("commons.delete") }}</deBtn
>
&nbsp;
</el-col>
<el-col class="right-user" :span="12">
<el-select
class="name-email-search"
v-model="selectType"
size="small"
@change="typeChange"
>
<el-option
v-for="(item, index) in $store.getters.msgTypes.filter(
(type) => type.pid <= 0
)"
:key="index"
:label="$t('webmsg.' + item.typeName)"
:value="item.msgTypeId"
></el-option>
</el-select>
</el-col>
</el-row>
<div class="table-container" :key="tabActive">
<grid-table
:key="tabActive"
:tableData="data"
:multipleSelection="multipleSelection"
:columns="[]"
:pagination="paginationConfig"
@selection-change="handleSelectionChange"
@sort-change="sortChange"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<el-table-column type="selection" width="55" />
<el-radio-group v-model="selectType" style="margin-bottom: 15px;" @change="typeChange"> <el-table-column prop="content" :label="$t('webmsg.content')">
<el-radio-button v-for="(item,index) in $store.getters.msgTypes.filter(type => type.pid <= 0)" :key="index" class="de-msg-radio-class" :label="item.msgTypeId">{{ $t('webmsg.' + item.typeName) }}</el-radio-button> <template slot-scope="scope">
<span style="display: flex; flex: 1">
<span>
<svg-icon
v-if="!scope.row.status"
icon-class="unread-msg"
style="color: red"
/>
<svg-icon v-else icon-class="readed-msg" />
</span>
<span
style="margin-left: 6px"
class="de-msg-a"
@click="toDetail(scope.row)"
>
{{ scope.row.content }}
</span>
</span>
</template>
</el-table-column>
</el-radio-group> <el-table-column
<complex-table prop="createTime"
:data="data" sortable="custom"
:columns="columns" :label="$t('webmsg.sned_time')"
:pagination-config="paginationConfig" width="180"
@select="select" >
@search="search" <template slot-scope="scope">
@sort-change="sortChange" <span>{{ scope.row.createTime | timestampFormatDate }}</span>
> </template>
</el-table-column>
<el-table-column prop="content" :label="$t('webmsg.content')"> <el-table-column
<template slot-scope="scope"> prop="typeId"
sortable="custom"
<span style="display: flex;flex: 1;"> :label="$t('webmsg.type')"
<span> width="140"
<svg-icon v-if="!scope.row.status" icon-class="unread-msg" style="color: red;" /> >
<svg-icon v-else icon-class="readed-msg" /> <template slot-scope="scope">
</span> <span>{{ getTypeName(scope.row.typeId) }}</span>
<span style="margin-left: 6px;" class="de-msg-a" @click="toDetail(scope.row)"> </template>
{{ scope.row.content }} </el-table-column>
</span> </grid-table>
</span> </div>
</div>
</template> </div>
</el-table-column> </div>
</de-layout-content>
<el-table-column prop="createTime" sortable="custom" :label="$t('webmsg.sned_time')" width="180">
<template slot-scope="scope">
<span>{{ scope.row.createTime | timestampFormatDate }}</span>
</template>
</el-table-column>
<el-table-column prop="typeId" sortable="custom" :label="$t('webmsg.type')" width="140">
<template slot-scope="scope">
<span>{{ getTypeName(scope.row.typeId) }}</span>
</template>
</el-table-column>
</complex-table>
</layout-content>
</template> </template>
<script> <script>
import DeLayoutContent from "@/components/business/DeLayoutContent";
import LayoutContent from '@/components/business/LayoutContent' import GridTable from "@/components/gridTable/index.vue";
import ComplexTable from '@/components/business/complex-table' import { query, updateStatus, batchRead, allRead, batchDelete } from '@/api/system/msg'
import { query, updateStatus } from '@/api/system/msg' import { msgTypes, getTypeName, loadMsgTypes } from "@/utils/webMsg";
import { msgTypes, getTypeName, loadMsgTypes } from '@/utils/webMsg' import bus from "@/utils/bus";
import bus from '@/utils/bus' import { addOrder, formatOrders } from "@/utils/index";
import { addOrder, formatOrders } from '@/utils/index' import msgCfm from "@/components/msgCfm/index";
import { mapGetters } from 'vuex' import { mapGetters } from "vuex";
export default { export default {
components: { components: {
LayoutContent, DeLayoutContent,
ComplexTable GridTable,
}, },
mixins: [msgCfm],
data() { data() {
return { return {
multipleSelection: [],
tabActive: "unread",
selectType: -1, selectType: -1,
msgTypes: msgTypes, msgTypes: msgTypes,
data: [], data: [],
allTypes: [{ name: 'mysql', type: 'jdbc' }, { name: 'sqlServer', type: 'jdbc' }], allTypes: [
{ name: "mysql", type: "jdbc" },
{ name: "sqlServer", type: "jdbc" },
],
columns: [], columns: [],
paginationConfig: { paginationConfig: {
currentPage: 1, currentPage: 1,
pageSize: 10, pageSize: 10,
total: 0 total: 0,
}, },
orderConditions: [] orderConditions: [],
} };
}, },
computed: { computed: {
...mapGetters([ ...mapGetters(["permission_routes"]),
'permission_routes'
])
}, },
mounted() { mounted() {
this.search() this.search();
}, },
created() { created() {
// //
loadMsgTypes() loadMsgTypes();
}, },
methods: { methods: {
select(selection) { changeTab(val) {
this.initSearch();
},
handleSelectionChange(val) {
this.multipleSelection = val;
},
handleSizeChange(pageSize) {
this.paginationConfig.currentPage = 1;
this.paginationConfig.pageSize = pageSize;
this.search();
},
handleCurrentChange(currentPage) {
this.paginationConfig.currentPage = currentPage;
this.search();
},
initSearch() {
this.handleCurrentChange(1);
},
allMarkReaded() {
allRead().then(res => {
this.openMessageSuccess('webmsg.mark_success');
bus.$emit('refresh-top-notification')
this.initSearch()
})
},
markReaded() {
const param = this.multipleSelection.map(item => item.msgId)
batchRead(param).then(res => {
this.openMessageSuccess('webmsg.mark_success');
bus.$emit('refresh-top-notification')
this.initSearch()
})
},
deleteBatch() {
const param = this.multipleSelection.map(item => item.msgId)
batchDelete(param).then(res => {
this.openMessageSuccess('commons.delete_success');
this.initSearch()
})
}, },
search() { search() {
const param = {} const param = {};
if (this.selectType >= 0) { if (this.selectType >= 0) {
param.type = this.selectType param.type = this.selectType;
} }
if (this.orderConditions.length === 0) { if (this.orderConditions.length === 0) {
param.orders = ['create_time desc '] param.orders = ["create_time desc "];
} else { } else {
param.orders = formatOrders(this.orderConditions) param.orders = formatOrders(this.orderConditions);
} }
const { currentPage, pageSize } = this.paginationConfig if (this.tabActive !== "allMsg") {
query(currentPage, pageSize, param).then(response => { param.status = this.tabActive === "readed";
this.data = response.data.listObject }
this.paginationConfig.total = response.data.itemCount
}) const { currentPage, pageSize } = this.paginationConfig;
query(currentPage, pageSize, param).then((response) => {
this.data = response.data.listObject;
this.paginationConfig.total = response.data.itemCount;
});
}, },
getTypeName(value) { getTypeName(value) {
return this.$t('webmsg.' + getTypeName(value)) return this.$t("webmsg." + getTypeName(value));
}, },
typeChange(value) { typeChange() {
this.search() this.initSearch();
}, },
toDetail(row) { toDetail(row) {
const param = { ...{ msgNotification: true, msgType: row.typeId, sourceParam: row.param }} const param = {
...{
msgNotification: true,
msgType: row.typeId,
sourceParam: row.param,
},
};
if (this.hasPermissionRoute(row.router)) { if (this.hasPermissionRoute(row.router)) {
this.$router.push({ name: row.router, params: param }) this.$router.push({ name: row.router, params: param });
row.status || this.setReaded(row) row.status || this.setReaded(row);
return return;
} }
this.$warning(this.$t('commons.no_target_permission')) this.$warning(this.$t("commons.no_target_permission"));
}, },
hasPermissionRoute(name, permission_routes) { hasPermissionRoute(name, permission_routes) {
permission_routes = permission_routes || this.permission_routes permission_routes = permission_routes || this.permission_routes;
for (let index = 0; index < permission_routes.length; index++) { for (let index = 0; index < permission_routes.length; index++) {
const route = permission_routes[index] const route = permission_routes[index];
if (route.name && route.name === name) return true if (route.name && route.name === name) return true;
if (route.children && this.hasPermissionRoute(name, route.children)) return true if (route.children && this.hasPermissionRoute(name, route.children))
return true;
} }
return false return false;
}, },
// //
setReaded(row) { setReaded(row) {
updateStatus(row.msgId).then(res => { updateStatus(row.msgId).then((res) => {
bus.$emit('refresh-top-notification') bus.$emit("refresh-top-notification");
this.search() this.search();
}) });
}, },
sortChange({ column, prop, order }) { sortChange({ column, prop, order }) {
this.orderConditions = [] this.orderConditions = [];
if (!order) { if (!order) {
this.search() this.search();
return return;
} }
if (prop === 'createTime') { if (prop === "createTime") {
prop = 'create_time' prop = "create_time";
} }
if (prop === 'typeId') { if (prop === "typeId") {
prop = 'type_id' prop = "type_id";
} }
addOrder({ field: prop, value: order }, this.orderConditions) addOrder({ field: prop, value: order }, this.orderConditions);
this.search() this.search();
} },
} },
};
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.de-msg-radio-class {
padding: 0 5px;
::v-deep .el-radio-button__inner {
border-radius: 4px 4px 4px 4px !important;
border-left: 1px solid #dcdfe6 !important;
padding: 10px 10px;
}
::v-deep .el-radio-button__orig-radio:checked+.el-radio-button__inner {
color: #fff;
// background-color: #0a7be0;
// border-color: #0a7be0;
-webkit-box-shadow: 0px 0 0 0 #0a7be0;
box-shadow: 0px 0 0 0 #0a7be0;
}
}
.de-msg-a:hover { .de-msg-a:hover {
text-decoration: underline !important; text-decoration: underline !important;
color: #0a7be0 !important; color: #0a7be0 !important;
cursor: pointer !important; cursor: pointer !important;
} }
.table-container {
height: calc(100% - 50px);
}
.top-operate {
margin-bottom: 16px;
.right-user {
text-align: right;
display: flex;
align-items: center;
justify-content: flex-end;
}
.name-email-search {
width: 240px;
}
}
</style> </style>
<style scoped lang="scss">
.organization {
height: 100%;
background-color: var(--MainBG, #f5f6f7);
.tabs-container {
height: calc(100% - 48px);
background: var(--ContentBG, #ffffff);
overflow-x: auto;
.msg-cont {
padding: 24px;
height: 100%;
box-sizing: border-box;
}
}
}
</style>

View File

@ -1,221 +0,0 @@
<template>
<layout-content v-loading="$store.getters.loadingMap[$store.getters.currentPath]">
<el-radio-group v-model="selectType" style="margin-bottom: 15px;" @change="typeChange">
<el-radio-button v-for="(item,index) in $store.getters.msgTypes.filter(type => type.pid <= 0)" :key="index" class="de-msg-radio-class" :label="item.msgTypeId">{{ $t('webmsg.' + item.typeName) }}</el-radio-button>
</el-radio-group>
<complex-table
:data="data"
:columns="columns"
:hide-columns="true"
:pagination-config="paginationConfig"
:search-config="searchConfig"
@select="select"
@search="search"
@selection-change="handleSelectionChange"
@sort-change="sortChange"
>
<template #toolbar>
<el-button :disabled="multipleSelection.length === 0" @click="deleteBatch">{{ $t('commons.delete') }}</el-button>
</template>
<el-table-column
type="selection"
width="55"
/>
<el-table-column prop="content" :label="$t('webmsg.content')">
<template slot-scope="scope">
<span style="display: flex;flex: 1;">
<span>
<svg-icon v-if="!scope.row.status" icon-class="unread-msg" style="color: red;" />
<svg-icon v-else icon-class="readed-msg" />
</span>
<span style="margin-left: 6px;" class="de-msg-a" @click="toDetail(scope.row)">
{{ scope.row.content }}
</span>
</span>
</template>
</el-table-column>
<el-table-column prop="createTime" sortable="custom" :label="$t('webmsg.sned_time')" width="180">
<template slot-scope="scope">
<span>{{ scope.row.createTime | timestampFormatDate }}</span>
</template>
</el-table-column>
<el-table-column prop="readTime" sortable="custom" :label="$t('webmsg.read_time')" width="180">
<template slot-scope="scope">
<span>{{ scope.row.readTime | timestampFormatDate }}</span>
</template>
</el-table-column>
<el-table-column prop="typeId" sortable="custom" :label="$t('webmsg.type')" width="140">
<template slot-scope="scope">
<span>{{ getTypeName(scope.row.typeId) }}</span>
</template>
</el-table-column>
</complex-table>
</layout-content>
</template>
<script>
import LayoutContent from '@/components/business/LayoutContent'
import ComplexTable from '@/components/business/complex-table'
import { query, batchDelete } from '@/api/system/msg'
import { msgTypes, getTypeName, loadMsgTypes } from '@/utils/webMsg'
import { addOrder, formatOrders } from '@/utils/index'
import { mapGetters } from 'vuex'
export default {
components: {
LayoutContent,
ComplexTable
},
data() {
return {
selectType: -1,
msgTypes: msgTypes,
data: [],
allTypes: [{ name: 'mysql', type: 'jdbc' }, { name: 'sqlServer', type: 'jdbc' }],
columns: [],
orderConditions: [],
paginationConfig: {
currentPage: 1,
pageSize: 10,
total: 0
},
multipleSelection: [],
searchConfig: {
useQuickSearch: false,
useComplexSearch: false
}
}
},
computed: {
...mapGetters([
'permission_routes'
])
},
mounted() {
this.search()
},
created() {
//
loadMsgTypes()
},
methods: {
select(selection) {
},
search() {
const param = {}
param.status = true
if (this.selectType >= 0) {
param.type = this.selectType
}
if (this.orderConditions.length === 0) {
param.orders = [' create_time desc ']
} else {
param.orders = formatOrders(this.orderConditions)
}
const { currentPage, pageSize } = this.paginationConfig
query(currentPage, pageSize, param).then(response => {
this.data = response.data.listObject
this.paginationConfig.total = response.data.itemCount
})
},
getTypeName(value) {
return this.$t('webmsg.' + getTypeName(value))
},
typeChange(value) {
this.search()
},
toDetail(row) {
const param = { ...{ msgNotification: true, msgType: row.typeId, sourceParam: row.param }}
// this.$router.push({ name: row.router, params: param })
if (this.hasPermissionRoute(row.router)) {
this.$router.push({ name: row.router, params: param })
return
}
this.$warning(this.$t('commons.no_target_permission'))
},
hasPermissionRoute(name, permission_routes) {
permission_routes = permission_routes || this.permission_routes
for (let index = 0; index < permission_routes.length; index++) {
const route = permission_routes[index]
if (route.name && route.name === name) return true
if (route.children && this.hasPermissionRoute(name, route.children)) return true
}
return false
},
sortChange({ column, prop, order }) {
this.orderConditions = []
if (!order) {
this.search()
return
}
if (prop === 'createTime') {
prop = 'create_time'
}
if (prop === 'readTime') {
prop = 'read_time'
}
if (prop === 'typeId') {
prop = 'type_id'
}
addOrder({ field: prop, value: order }, this.orderConditions)
this.search()
},
deleteBatch() {
if (this.multipleSelection.length === 0) {
this.$warning(this.$t('webmsg.please_select'))
return
}
const param = this.multipleSelection.map(item => item.msgId)
batchDelete(param).then(res => {
this.$success(this.$t('commons.delete_success'))
this.search()
})
},
handleSelectionChange(val) {
this.multipleSelection = val
}
}
}
</script>
<style lang="scss" scoped>
.de-msg-radio-class {
padding: 0 5px;
::v-deep .el-radio-button__inner {
border-radius: 4px 4px 4px 4px !important;
border-left: 1px solid #dcdfe6 !important;
padding: 10px 10px;
}
::v-deep .el-radio-button__orig-radio:checked+.el-radio-button__inner {
color: #fff;
/* background-color: #0a7be0;
border-color: #0a7be0; */
-webkit-box-shadow: 0px 0 0 0 #0a7be0;
box-shadow: 0px 0 0 0 #0a7be0;
}
}
.de-msg-a:hover {
text-decoration: underline !important;
color: #0a7be0 !important;
cursor: pointer !important;
}
</style>

View File

@ -1,13 +1,19 @@
<template xmlns:el-col="http://www.w3.org/1999/html"> <template xmlns:el-col="http://www.w3.org/1999/html">
<layout-content :header="$t('webmsg.receive_manage')"> <de-layout-content :header="$t('消息接收管理')">
<el-col> <el-col>
<el-row class="tree-head"> <el-row class="tree-head">
<span style="float: left;padding-left: 10px">{{ $t('webmsg.type') }}</span> <span style="float: left;">{{
<span v-for="channel in msg_channels" :key="channel.msgChannelId" class="auth-span"> $t("webmsg.type")
}}</span>
<span
v-for="channel in msg_channels"
:key="channel.msgChannelId"
class="auth-span"
>
{{ $t(channel.channelName) }} {{ $t(channel.channelName) }}
</span> </span>
</el-row> </el-row>
<el-row style="margin-top: 5px"> <el-row class="msg-setting" style="margin-top: 5px">
<el-tree <el-tree
:props="defaultProps" :props="defaultProps"
:data="treeData" :data="treeData"
@ -18,235 +24,281 @@
> >
<span slot-scope="{ node, data }" class="custom-tree-node"> <span slot-scope="{ node, data }" class="custom-tree-node">
<span> <span>
<span style="margin-left: 6px">{{ $t('webmsg.' + data.name) }}</span> <span style="margin-left: 6px">{{
$t("webmsg." + data.name)
}}</span>
</span> </span>
<span @click.stop> <span @click.stop>
<div> <div>
<span v-for="channel in msg_channels" :key="channel.msgChannelId" class="auth-span"> <span
v-for="channel in msg_channels"
<el-checkbox v-if="data.children && data.children.length > 0" v-model="data.check_all_map[channel.msgChannelId]" :indeterminate="data.indeterminate_map[channel.msgChannelId]" @change="parentBoxChange(node, channel)" /> :key="channel.msgChannelId"
<el-checkbox v-else v-model="data.check_map[channel.msgChannelId]" @change="childBoxChange(node, channel)" /> class="auth-span-check"
>
</span> <el-checkbox
</div></span> v-if="data.children && data.children.length > 0"
v-model="data.check_all_map[channel.msgChannelId]"
:indeterminate="
data.indeterminate_map[channel.msgChannelId]
"
@change="parentBoxChange(node, channel)"
/>
<el-checkbox
v-else
v-model="data.check_map[channel.msgChannelId]"
@change="childBoxChange(node, channel)"
/>
</span></div
></span>
</span> </span>
</el-tree> </el-tree>
</el-row> </el-row>
</el-col> </el-col>
</layout-content> </de-layout-content>
</template> </template>
<script> <script>
import LayoutContent from '@/components/business/LayoutContent' import DeLayoutContent from "@/components/business/DeLayoutContent";
import { treeList, channelList, settingList, updateSetting, batchUpdate } from '@/api/system/msg' import {
treeList,
channelList,
settingList,
updateSetting,
batchUpdate,
} from "@/api/system/msg";
export default { export default {
name: 'LazyTree', name: "LazyTree",
components: { LayoutContent }, components: { DeLayoutContent },
data() { data() {
return { return {
treeData: [], treeData: [],
defaultProps: { defaultProps: {
children: 'children', children: "children",
label: 'name', label: "name",
id: 'id' id: "id",
}, },
highlightCurrent: true, highlightCurrent: true,
msg_channels: [], msg_channels: [],
setting_data: {} setting_data: {},
} };
},
computed: {
},
mounted() {
}, },
computed: {},
mounted() {},
beforeCreate() { beforeCreate() {
// this.loadChannelData() // this.loadChannelData()
channelList().then(res => { channelList().then((res) => {
this.msg_channels = res.data this.msg_channels = res.data;
}) });
}, },
created() { created() {
this.loadSettingData(this.loadTreeData) this.loadSettingData(this.loadTreeData);
}, },
methods: { methods: {
// //
loadTreeData() { loadTreeData() {
treeList().then(res => { treeList().then((res) => {
const datas = res.data const datas = res.data;
datas.forEach(data => this.formatTreeNode(data)) datas.forEach((data) => this.formatTreeNode(data));
this.treeData = datas this.treeData = datas;
}) });
}, },
formatTreeNode(node) { formatTreeNode(node) {
if (node.children && node.children.length > 0) { if (node.children && node.children.length > 0) {
node.check_all_map = {} node.check_all_map = {};
node.indeterminate_map = {} node.indeterminate_map = {};
node.indeterminate_number_map = {} node.indeterminate_number_map = {};
const kidSize = node.children.length const kidSize = node.children.length;
node.children.forEach(kid => { node.children.forEach((kid) => {
this.formatTreeNode(kid) this.formatTreeNode(kid);
const isLeaf = !kid.children || kid.children.length === 0 const isLeaf = !kid.children || kid.children.length === 0;
const tempMap = isLeaf ? kid.check_map : kid.indeterminate_map const tempMap = isLeaf ? kid.check_map : kid.indeterminate_map;
for (const key in tempMap) { for (const key in tempMap) {
if (Object.hasOwnProperty.call(tempMap, key)) { if (Object.hasOwnProperty.call(tempMap, key)) {
const element = tempMap[key] const element = tempMap[key];
node.indeterminate_number_map[key] = node.indeterminate_number_map[key] || 0 node.indeterminate_number_map[key] =
node.indeterminate_number_map[key] || 0;
if (element) { if (element) {
node.indeterminate_number_map[key]++ node.indeterminate_number_map[key]++;
} }
if (node.indeterminate_number_map[key] === kidSize && (isLeaf || kid.check_all_map[key])) { if (
node.check_all_map[key] = true node.indeterminate_number_map[key] === kidSize &&
node.indeterminate_map[key] = false (isLeaf || kid.check_all_map[key])
) {
node.check_all_map[key] = true;
node.indeterminate_map[key] = false;
} else if (node.indeterminate_number_map[key] > 0) { } else if (node.indeterminate_number_map[key] > 0) {
node.check_all_map[key] = false node.check_all_map[key] = false;
node.indeterminate_map[key] = true node.indeterminate_map[key] = true;
} }
} }
} }
}) });
} else { } else {
node.check_map = {} node.check_map = {};
this.msg_channels.forEach(channel => { this.msg_channels.forEach((channel) => {
node.check_map[channel.msgChannelId] = this.checkBoxStatus(node, channel) node.check_map[channel.msgChannelId] = this.checkBoxStatus(
}) node,
channel
);
});
// this.checkBoxStatus(node, ) // this.checkBoxStatus(node, )
} }
}, },
// //
loadChannelData() { loadChannelData() {
channelList().then(res => { channelList().then((res) => {
this.msg_channels = res.data this.msg_channels = res.data;
}) });
}, },
// //
loadSettingData(callBack) { loadSettingData(callBack) {
// this.setting_data = {} // this.setting_data = {}
const temp_setting_data = {} const temp_setting_data = {};
settingList().then(res => { settingList().then((res) => {
const lists = res.data const lists = res.data;
lists.forEach(item => { lists.forEach((item) => {
const key = item.typeId + '' const key = item.typeId + "";
if (!Object.keys(temp_setting_data).includes(key)) { if (!Object.keys(temp_setting_data).includes(key)) {
temp_setting_data[key] = [] temp_setting_data[key] = [];
} }
temp_setting_data[key].push(item) temp_setting_data[key].push(item);
}) });
this.setting_data = temp_setting_data this.setting_data = temp_setting_data;
callBack && callBack() callBack && callBack();
}) });
}, },
checkBoxStatus(node, channel) { checkBoxStatus(node, channel) {
// const nodeId = node.data.id // const nodeId = node.data.id
const nodeId = node.id const nodeId = node.id;
return this.setting_data[nodeId] && this.setting_data[nodeId].some(item => item.channelId === channel.msgChannelId && item.enable) return (
this.setting_data[nodeId] &&
this.setting_data[nodeId].some(
(item) => item.channelId === channel.msgChannelId && item.enable
)
);
}, },
nodeClick(data, node) { nodeClick(data, node) {},
},
getAllKidId(node, ids) { getAllKidId(node, ids) {
if (node.children && node.children.length > 0) { if (node.children && node.children.length > 0) {
node.children.forEach(item => this.getAllKidId(item, ids)) node.children.forEach((item) => this.getAllKidId(item, ids));
} else { } else {
ids.push(node.id) ids.push(node.id);
} }
}, },
parentBoxChange(node, channel) { parentBoxChange(node, channel) {
const typeIds = [] const typeIds = [];
this.getAllKidId(node.data, typeIds) this.getAllKidId(node.data, typeIds);
const channelId = channel.msgChannelId const channelId = channel.msgChannelId;
const data = node.data const data = node.data;
const enable = data.check_all_map && data.check_all_map[channelId] const enable = data.check_all_map && data.check_all_map[channelId];
node.data.check_all_map[channelId] = enable node.data.check_all_map[channelId] = enable;
node.data.indeterminate_map[channelId] = false node.data.indeterminate_map[channelId] = false;
node.data.children.forEach(item => { node.data.children.forEach((item) => {
item.check_map = item.check_map || {} item.check_map = item.check_map || {};
item.check_map[channelId] = enable item.check_map[channelId] = enable;
}) });
const param = { const param = {
typeIds: typeIds, typeIds: typeIds,
channelId: channelId, channelId: channelId,
enable enable,
} };
batchUpdate(param).then(res => { batchUpdate(param).then((res) => {
this.loadSettingData(this.loadTreeData) this.loadSettingData(this.loadTreeData);
}) });
}, },
childBoxChange(node, channel) { childBoxChange(node, channel) {
const channelId = channel.msgChannelId const channelId = channel.msgChannelId;
const parent = node.parent const parent = node.parent;
if (parent) { if (parent) {
const data = parent.data const data = parent.data;
const kids = data.children const kids = data.children;
const kidSize = kids.length const kidSize = kids.length;
let index = 0 let index = 0;
kids.forEach(kid => { kids.forEach((kid) => {
if (kid.check_map[channelId]) { if (kid.check_map[channelId]) {
index++ index++;
} }
}) });
if (index === kidSize) { if (index === kidSize) {
node.parent.data.check_all_map[channelId] = true node.parent.data.check_all_map[channelId] = true;
node.parent.data.indeterminate_map[channelId] = false node.parent.data.indeterminate_map[channelId] = false;
} else if (index > 0) { } else if (index > 0) {
node.parent.data.check_all_map[channelId] = false node.parent.data.check_all_map[channelId] = false;
node.parent.data.indeterminate_map[channelId] = true node.parent.data.indeterminate_map[channelId] = true;
} else { } else {
node.parent.data.check_all_map[channelId] = false node.parent.data.check_all_map[channelId] = false;
node.parent.data.indeterminate_map[channelId] = false node.parent.data.indeterminate_map[channelId] = false;
} }
// this.formatTreeNode(node.parent.data) // this.formatTreeNode(node.parent.data)
} }
const param = { const param = {
typeId: node.data.id, typeId: node.data.id,
channelId: channelId channelId: channelId,
};
updateSetting(param).then((res) => {
this.loadSettingData(this.loadTreeData);
});
},
},
};
</script>
<style scoped lang="scss">
.custom-tree-node {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
padding-left: 8px;
padding-right: 32px;
}
.tree-main {
overflow-y: auto;
}
.tree-head {
height: 46px;
line-height: 46px;
border-bottom: 1px solid var(--TableBorderColor, #e6e6e6);
border-top: 1px solid var(--TableBorderColor, #e6e6e6);
background-color: var(--SiderBG, #f7f8fa);
font-size: 12px;
color: var(--TableColor, #3d4d66);
font-family: PingFang SC;
font-size: 14px;
font-weight: 500;
padding: 0 12px
}
.auth-span {
float: right;
margin-left: 24px;
}
.auth-span-check {
float: right;
margin-left: 64px;
}
</style>
<style lang="scss">
.msg-setting {
.el-tree-node__content {
height: 46px;
border-bottom: 1px solid rgba(31, 35, 41, 0.15);
&:hover {
background-color: var(--deWhiteHover, #3370ff) !important;
.custom-tree-node {
color: var(--primary, #3370ff);
} }
updateSetting(param).then(res => {
this.loadSettingData(this.loadTreeData)
})
} }
} }
} }
</script> </style>
<style scoped>
.custom-tree-node {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
padding-left: 8px;
}
.tree-main{
overflow-y: auto;
}
.tree-head{
height: 30px;
line-height: 30px;
border-bottom: 1px solid var(--TableBorderColor, #e6e6e6);
background-color: var(--SiderBG, #f7f8fa);
font-size: 12px;
color: var(--TableColor, #3d4d66) ;
}
.auth-span{
float: right;
width:50px;
margin-right: 30px
}
.highlights-text {
color: #faaa39 !important;
}
</style>

View File

@ -1,229 +0,0 @@
<template>
<layout-content v-loading="$store.getters.loadingMap[$store.getters.currentPath]">
<el-radio-group v-model="selectType" style="margin-bottom: 15px;" @change="typeChange">
<el-radio-button v-for="(item,index) in $store.getters.msgTypes.filter(type => type.pid <= 0)" :key="index" class="de-msg-radio-class" :label="item.msgTypeId">{{ $t('webmsg.' + item.typeName) }}</el-radio-button>
</el-radio-group>
<complex-table
:data="data"
:columns="columns"
:hide-columns="true"
:pagination-config="paginationConfig"
:search-config="searchConfig"
@select="select"
@search="search"
@selection-change="handleSelectionChange"
@sort-change="sortChange"
>
<template #toolbar>
<el-button :disabled="multipleSelection.length === 0" @click="markReaded">{{ $t('webmsg.mark_readed') }}</el-button>
<el-button @click="allMarkReaded">{{ $t('webmsg.all_mark_readed') }}</el-button>
</template>
<el-table-column
type="selection"
width="55"
/>
<el-table-column prop="content" :label="$t('webmsg.content')">
<template slot-scope="scope">
<span style="display: flex;flex: 1;">
<span>
<svg-icon v-if="!scope.row.status" icon-class="unread-msg" style="color: red;" />
<svg-icon v-else icon-class="readed-msg" />
</span>
<span style="margin-left: 6px;" class="de-msg-a" @click="toDetail(scope.row)">
{{ scope.row.content }}
</span>
</span>
</template>
</el-table-column>
<el-table-column prop="createTime" sortable="custom" :label="$t('webmsg.sned_time')" width="180">
<template slot-scope="scope">
<span>{{ scope.row.createTime | timestampFormatDate }}</span>
</template>
</el-table-column>
<el-table-column prop="typeId" sortable="custom" :label="$t('webmsg.type')" width="140">
<template slot-scope="scope">
<span>{{ getTypeName(scope.row.typeId) }}</span>
</template>
</el-table-column>
</complex-table>
</layout-content>
</template>
<script>
import LayoutContent from '@/components/business/LayoutContent'
import ComplexTable from '@/components/business/complex-table'
import { query, updateStatus, batchRead, allRead } from '@/api/system/msg'
import { msgTypes, getTypeName, loadMsgTypes } from '@/utils/webMsg'
import bus from '@/utils/bus'
import { addOrder, formatOrders } from '@/utils/index'
import { mapGetters } from 'vuex'
export default {
components: {
LayoutContent,
ComplexTable
},
data() {
return {
selectType: -1,
msgTypes: msgTypes,
data: [],
allTypes: [{ name: 'mysql', type: 'jdbc' }, { name: 'sqlServer', type: 'jdbc' }],
columns: [],
paginationConfig: {
currentPage: 1,
pageSize: 10,
total: 0
},
searchConfig: {
useQuickSearch: false,
useComplexSearch: false
},
multipleSelection: [],
orderConditions: []
}
},
computed: {
...mapGetters([
'permission_routes'
])
},
mounted() {
this.search()
},
created() {
//
loadMsgTypes()
},
methods: {
select(selection) {
},
search() {
const param = {}
param.status = false
if (this.selectType >= 0) {
param.type = this.selectType
}
if (this.orderConditions.length === 0) {
param.orders = [' create_time desc ']
} else {
param.orders = formatOrders(this.orderConditions)
}
const { currentPage, pageSize } = this.paginationConfig
query(currentPage, pageSize, param).then(response => {
this.data = response.data.listObject
this.paginationConfig.total = response.data.itemCount
})
},
getTypeName(value) {
return this.$t('webmsg.' + getTypeName(value))
},
typeChange(value) {
this.search()
},
toDetail(row) {
const param = { ...{ msgNotification: true, msgType: row.typeId, sourceParam: row.param }}
if (this.hasPermissionRoute(row.router)) {
this.$router.push({ name: row.router, params: param })
this.setReaded(row)
return
}
this.$warning(this.$t('commons.no_target_permission'))
},
hasPermissionRoute(name, permission_routes) {
permission_routes = permission_routes || this.permission_routes
for (let index = 0; index < permission_routes.length; index++) {
const route = permission_routes[index]
if (route.name && route.name === name) return true
if (route.children && this.hasPermissionRoute(name, route.children)) return true
}
return false
},
//
setReaded(row) {
updateStatus(row.msgId).then(res => {
bus.$emit('refresh-top-notification')
this.search()
})
},
allMarkReaded() {
allRead().then(res => {
this.$success(this.$t('webmsg.mark_success'))
bus.$emit('refresh-top-notification')
this.search()
})
},
markReaded() {
if (this.multipleSelection.length === 0) {
this.$warning(this.$t('webmsg.please_select'))
return
}
const param = this.multipleSelection.map(item => item.msgId)
batchRead(param).then(res => {
this.$success(this.$t('webmsg.mark_success'))
bus.$emit('refresh-top-notification')
this.search()
})
},
handleSelectionChange(val) {
this.multipleSelection = val
},
sortChange({ column, prop, order }) {
this.orderConditions = []
if (!order) {
this.search()
return
}
if (prop === 'createTime') {
prop = 'create_time'
}
if (prop === 'typeId') {
prop = 'type_id'
}
addOrder({ field: prop, value: order }, this.orderConditions)
this.search()
}
}
}
</script>
<style lang="scss" scoped>
.de-msg-radio-class {
padding: 0 5px;
::v-deep .el-radio-button__inner {
border-radius: 4px 4px 4px 4px !important;
border-left: 1px solid #dcdfe6 !important;
padding: 10px 10px;
}
::v-deep .el-radio-button__orig-radio:checked+.el-radio-button__inner {
color: #fff;
/* background-color: #0a7be0;
border-color: #0a7be0; */
-webkit-box-shadow: 0px 0 0 0 #0a7be0;
box-shadow: 0px 0 0 0 #0a7be0;
}
}
.de-msg-a:hover {
text-decoration: underline !important;
color: #0a7be0 !important;
cursor: pointer !important;
}
</style>

View File

@ -1,165 +1,361 @@
<template> <template>
<layout-content v-loading="$store.getters.loadingMap[$store.getters.currentPath]"> <de-layout-content
<complex-table v-loading="$store.getters.loadingMap[$store.getters.currentPath]"
:data="data" >
:columns="columns" <div class="top-install">
:search-config="searchConfig" <el-input
:pagination-config="paginationConfig" placeholder="通过插件名称搜索"
@search="search" size="small"
> prefix-icon="el-icon-search"
<template #toolbar> v-model="name"
<el-upload clearable
v-permission="['plugin:upload']" @blur="search"
:action="baseUrl+'api/plugin/upload'" >
:multiple="false" </el-input>
:show-file-list="false" <el-upload
:file-list="fileList" v-permission="['plugin:upload']"
accept=".zip" :action="baseUrl + 'api/plugin/upload'"
:before-upload="beforeUpload" :multiple="false"
:on-success="uploadSuccess" :show-file-list="false"
:on-error="uploadFail" :file-list="fileList"
name="file" accept=".zip"
:headers="headers" :before-upload="beforeUpload"
:on-success="uploadSuccess"
:on-error="uploadFail"
name="file"
:headers="headers"
>
<deBtn
:icon="!uploading ? 'el-icon-upload2' : 'el-icon-loading'"
type="primary"
:disabled="uploading"
> >
<el-button size="mini" type="primary" :disabled="uploading"> {{ $t(!uploading ? "plugin.local_install" : "dataset.uploading") }}
<span v-if="!uploading" style="font-size: 12px;">{{ $t('plugin.local_install') }}</span> </deBtn>
<span v-if="uploading" style="font-size: 12px;"><i class="el-icon-loading" /> {{ $t('dataset.uploading') }}</span> </el-upload>
</el-button> </div>
</el-upload> <div v-if="!data.length" class="plugin-cont">
</template> <el-empty style="width: 100%" description="没有找到相关内容"></el-empty>
</div>
<el-table-column prop="name" :label="$t('plugin.name')" /> <div v-else class="plugin-cont">
<!-- <el-table-column prop="free" :label="$t('plugin.free')"> <div v-for="ele in data" :key="ele.pluginId" class="de-card-plugin">
<template v-slot:default="scope"> <div class="card-info">
<span>{{ scope.row.free ? '是' : '否' }}</span> <div class="info-top">
</template> <img
</el-table-column> --> src="https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg"
<el-table-column prop="cost" :label="$t('plugin.cost')" /> alt=""
/>
<el-table-column :show-overflow-tooltip="true" prop="descript" :label="$t('plugin.descript')" /> <p class="title">{{ ele.descript }}</p>
<el-table-column prop="version" :label="$t('plugin.version')" /> <el-tooltip
<el-table-column prop="creator" :label="$t('plugin.creator')" /> class="item"
effect="dark"
<el-table-column prop="installTime" :label="$t('plugin.install_time')"> :content="ele.descript"
<template v-slot:default="scope"> placement="top"
<span>{{ scope.row.installTime | timestampFormatDate }}</span> >
</template> <p class="tips">{{ ele.name }}</p>
</el-table-column> </el-tooltip>
<fu-table-operations :buttons="buttons" :label="$t('commons.operating')" fix /> </div>
</complex-table> <div class="info-left">
<p class="list name" v-for="item in listName" :key="item">
</layout-content> {{ item }}
</p>
</div>
<div class="info-right">
<p class="list value" v-for="item in listValue" :key="item">
<template v-if="item === 'cost' && !ele.cost">
<el-tag size="mini" type="success">免费</el-tag>
</template>
<template v-else>
{{ ele[item] }}
</template>
</p>
</div>
</div>
<div class="card-method">
<el-upload
v-permission="['plugin:upload']"
:action="baseUrl + 'api/plugin/upload'"
:multiple="false"
:show-file-list="false"
:file-list="fileList"
accept=".zip"
:before-upload="beforeUpload"
:on-success="uploadSuccess"
:on-error="uploadFail"
name="file"
:headers="headers"
>
<div class="btn-plugin"><i class="el-icon-more"></i>更新</div>
</el-upload>
<el-divider direction="vertical"></el-divider>
<div
:class="[{ 'is-disable': btnDisabled(ele) }]"
v-show="checkPermission(['plugin:uninstall'])"
@click="del(ele)"
class="btn-plugin"
>
<i class="el-icon-more"></i>卸载
</div>
</div>
</div>
</div>
</de-layout-content>
</template> </template>
<script> <script>
import LayoutContent from '@/components/business/LayoutContent' import DeLayoutContent from "@/components/business/DeLayoutContent";
import ComplexTable from '@/components/business/complex-table'
import { checkPermission } from '@/utils/permission' import { checkPermission } from "@/utils/permission";
import { formatCondition, formatQuickCondition } from '@/utils/index' import { formatCondition, formatQuickCondition } from "@/utils/index";
import { pluginLists, uninstall } from '@/api/system/plugin' import { pluginLists, uninstall } from "@/api/system/plugin";
import { getToken } from '@/utils/auth' import { getToken } from "@/utils/auth";
import msgCfm from "@/components/msgCfm/index";
export default { export default {
components: { DeLayoutContent },
components: { ComplexTable, LayoutContent }, mixins: [msgCfm],
data() { data() {
return { return {
header: '', listName: ["费用", "开发者", "版本", "安装时间"],
columns: [], name: "",
buttons: [ listValue: ["cost", "creator", "version", "installTime"],
// {
// label: this.$t('commons.delete'), icon: 'el-icon-delete', type: 'danger', click: this.del,
// show: checkPermission(['user:del'])
// }
{
label: this.$t('plugin.un_install'), icon: 'el-icon-delete', type: 'danger', click: this.del,
show: checkPermission(['plugin:uninstall']),
disabled: this.btnDisabled
}
],
searchConfig: {
useQuickSearch: true,
quickPlaceholder: this.$t('role.search_by_name'),
components: [
{ field: 'name', label: this.$t('plugin.name'), component: 'DeComplexInput' }
]
},
paginationConfig: {
currentPage: 1,
pageSize: 10,
total: 0
},
data: [], data: [],
uploading: false, uploading: false,
baseUrl: process.env.VUE_APP_BASE_API, baseUrl: process.env.VUE_APP_BASE_API,
fileList: [], fileList: [],
headers: { Authorization: getToken() } headers: { Authorization: getToken() },
};
}
}, },
mounted() { mounted() {
this.search() this.search();
this.bindKey();
},
destroyed() {
this.unBindKey();
}, },
methods: { methods: {
entryKey(event) {
search(condition) { const keyCode = event.keyCode;
condition = formatQuickCondition(condition, 'name') if (keyCode === 13) {
const temp = formatCondition(condition) this.search();
const param = temp || {} }
const { currentPage, pageSize } = this.paginationConfig },
pluginLists(currentPage, pageSize, param).then(response => { bindKey() {
this.data = response.data.listObject document.addEventListener("keypress", this.entryKey);
this.paginationConfig.total = response.data.itemCount },
}) unBindKey() {
document.removeEventListener("keypress", this.entryKey);
},
search() {
const param = {};
if (this.name) {
param.conditions = [
{
field: "name",
operator: "like",
value: this.name,
},
];
}
pluginLists(0, 0, param).then((response) => {
this.data = response.data.listObject;
this.data.forEach((ele) => {
if (ele.installTime) {
ele.installTime = new Date(ele.installTime).format(
"yyyy-MM-dd hh:mm:ss"
);
}
if (ele.cost) {
ele.cost = ele.cost.toLocaleString();
}
});
});
}, },
beforeUpload(file) { beforeUpload(file) {
this.uploading = true this.uploading = true;
}, },
uploadFail(response, file, fileList) { uploadFail(response, file, fileList) {
const msg = response && response.message || '安装失败' const msg = (response && response.message) || "安装失败";
try { try {
const result = JSON.parse(msg) const result = JSON.parse(msg);
if (result && result.message) { if (result && result.message) {
this.$error(result.message) this.$error(result.message);
this.uploading = false this.uploading = false;
} }
return return;
} catch (e) { } catch (e) {
console.error(e) console.error(e);
} }
this.$error(msg) this.$error(msg);
this.uploading = false this.uploading = false;
}, },
uploadSuccess(response, file, fileList) { uploadSuccess(response, file, fileList) {
this.uploading = false this.uploading = false;
this.search() this.search();
}, },
del(row) { del(row) {
this.$confirm(this.$t('plugin.uninstall_confirm'), '', { const options = {
confirmButtonText: this.$t('commons.confirm'), title: "确定卸载该插件?",
cancelButtonText: this.$t('commons.cancel'), content: "卸载并重启服务器之后才能生效",
type: 'warning' confirmButtonText: this.$t('卸载'),
}).then(() => { type: "primary",
uninstall(row.pluginId).then(res => { cb: () => {
this.search() uninstall(row.pluginId)
this.$success(this.$t('plugin.un_install_success')) .then((res) => {
}).catch(() => { this.search();
this.$error(this.$t('plugin.un_install_error')) this.openMessageSuccess("plugin.un_install_success");
}) })
}).catch(() => { .catch(() => {
this.$info(this.$t('plugin.uninstall_cancel')) this.$error(this.$t("plugin.un_install_error"));
}) });
},
};
this.handlerConfirm(options);
}, },
btnDisabled(row) { btnDisabled(row) {
return row.pluginId < 4 return row.pluginId < 4;
} },
},
};
</script>
<style lang="scss">
.top-install {
position: absolute;
top: 24px;
right: 24px;
display: flex;
align-items: center;
justify-content: flex-end;
.el-input {
margin-right: 12px;
}
.el-input__inner {
background: #ffffff !important;
} }
} }
</script>
<style scoped> .plugin-cont {
height: 100%;
display: flex;
flex-wrap: wrap;
background-color: var(--MainBG, #f5f6f7);
overflow-y: auto;
}
.de-card-plugin {
width: 270px;
height: 230px;
background: #ffffff;
border: 1px solid #dee0e3;
border-radius: 4px;
margin: 0 24px 24px 0;
&:hover {
box-shadow: 0px 6px 24px rgba(31, 35, 41, 0.08);
}
.card-method {
border-top: 1px solid #dee0e3;
display: flex;
align-items: center;
padding: 9px 30px 10px 30px;
width: 100%;
justify-content: space-around;
box-sizing: border-box;
.btn-plugin {
font-family: "PingFang SC";
font-style: normal;
font-weight: 400;
font-size: 14px;
line-height: 22px;
/* identical to box height, or 157% */
display: flex;
align-items: center;
letter-spacing: -0.1px;
/* Neutral/600 */
color: #646a73;
i {
font-size: 13px;
margin-right: 5.33px;
}
}
}
.card-info {
width: 100%;
height: 188px;
padding: 12px;
padding-bottom: 4px;
box-sizing: border-box;
.info-top {
margin-bottom: 12px;
overflow: hidden;
img {
float: left;
box-sizing: border-box;
width: 40px;
height: 40px;
background: #ffffff;
border: 1px solid #dee0e3;
border-radius: 4px;
}
.title {
width: 190px;
height: 22px;
float: left;
font-family: "PingFang SC";
font-style: normal;
font-weight: 500;
font-size: 14px;
line-height: 22px;
color: #000000;
margin: -2px 0 0 6px;
}
.tips {
max-width: 200px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
float: left;
height: 20px;
font-family: "PingFang SC";
font-style: normal;
font-weight: 400;
font-size: 12px;
line-height: 20px;
color: #646a73;
margin: 2px 0 0 6px;
}
}
.list {
padding-bottom: 8px;
font-family: "PingFang SC";
font-style: normal;
font-weight: 400;
font-size: 14px;
line-height: 22px;
margin: 0;
color: #646a73;
}
.info-left {
display: inline-block;
.name {
color: #646a73;
}
}
.info-right {
display: inline-block;
margin-left: 12px;
.value {
color: #1f2329;
}
}
}
}
</style> </style>