feat: 财务管理

This commit is contained in:
tk
2025-06-26 17:35:19 +08:00
committed by nsnail
parent a202595687
commit 94d0b7028f
63 changed files with 2544 additions and 72 deletions

View File

@ -10,8 +10,8 @@
},
"dependencies": {
"@element-plus/icons-vue": "2.3.1",
"ace-builds": "1.42.0",
"aieditor": "1.3.9",
"ace-builds": "1.43.0",
"aieditor": "1.4.0",
"axios": "1.10.0",
"crypto-js": "4.2.0",
"dayjs": "1.11.13",
@ -23,8 +23,8 @@
"nprogress": "0.2.0",
"sortablejs": "1.15.6",
"vkbeautify": "0.99.3",
"vue": "3.5.16",
"vue-i18n": "11.1.6",
"vue": "3.5.17",
"vue-i18n": "11.1.7",
"vue-router": "4.5.1",
"vue3-ace-editor": "2.2.4",
"vue3-json-viewer": "2.4.0",
@ -32,12 +32,12 @@
"vuex": "4.1.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "5.2.4",
"prettier": "3.5.3",
"@vitejs/plugin-vue": "6.0.0",
"prettier": "3.6.1",
"prettier-plugin-organize-attributes": "1.0.0",
"sass": "1.89.2",
"terser": "5.43.0",
"vite": "6.3.5"
"terser": "5.43.1",
"vite": "7.0.0"
},
"browserslist": [
"> 1%",

View File

@ -0,0 +1,95 @@
/**
* 用户钱包服务
* @module @/api/sys/user.wallet
*/
import config from '@/config'
import http from '@/utils/request'
export default {
/**
* 批量删除用户钱包
*/
bulkDelete: {
url: `${config.API_URL}/api/sys/user.wallet/bulk.delete`,
name: `批量删除用户钱包`,
post: async function (data = {}, config = {}) {
return await http.post(this.url, data, config)
},
},
/**
* 用户钱包计数
*/
count: {
url: `${config.API_URL}/api/sys/user.wallet/count`,
name: `用户钱包计数`,
post: async function (data = {}, config = {}) {
return await http.post(this.url, data, config)
},
},
/**
* 用户钱包分组计数
*/
countBy: {
url: `${config.API_URL}/api/sys/user.wallet/count.by`,
name: `用户钱包分组计数`,
post: async function (data = {}, config = {}) {
return await http.post(this.url, data, config)
},
},
/**
* 创建用户钱包
*/
create: {
url: `${config.API_URL}/api/sys/user.wallet/create`,
name: `创建用户钱包`,
post: async function (data = {}, config = {}) {
return await http.post(this.url, data, config)
},
},
/**
* 删除用户钱包
*/
delete: {
url: `${config.API_URL}/api/sys/user.wallet/delete`,
name: `删除用户钱包`,
post: async function (data = {}, config = {}) {
return await http.post(this.url, data, config)
},
},
/**
* 编辑用户钱包
*/
edit: {
url: `${config.API_URL}/api/sys/user.wallet/edit`,
name: `编辑用户钱包`,
post: async function (data = {}, config = {}) {
return await http.post(this.url, data, config)
},
},
/**
* 获取单个用户钱包
*/
get: {
url: `${config.API_URL}/api/sys/user.wallet/get`,
name: `获取单个用户钱包`,
post: async function (data = {}, config = {}) {
return await http.post(this.url, data, config)
},
},
/**
* 分页查询用户钱包
*/
pagedQuery: {
url: `${config.API_URL}/api/sys/user.wallet/paged.query`,
name: `分页查询用户钱包`,
post: async function (data = {}, config = {}) {
return await http.post(this.url, data, config)
},
},
}

View File

@ -0,0 +1,95 @@
/**
* 钱包交易服务
* @module @/api/sys/wallet.trade
*/
import config from '@/config'
import http from '@/utils/request'
export default {
/**
* 批量删除钱包交易
*/
bulkDelete: {
url: `${config.API_URL}/api/sys/wallet.trade/bulk.delete`,
name: `批量删除钱包交易`,
post: async function (data = {}, config = {}) {
return await http.post(this.url, data, config)
},
},
/**
* 钱包交易计数
*/
count: {
url: `${config.API_URL}/api/sys/wallet.trade/count`,
name: `钱包交易计数`,
post: async function (data = {}, config = {}) {
return await http.post(this.url, data, config)
},
},
/**
* 钱包交易分组计数
*/
countBy: {
url: `${config.API_URL}/api/sys/wallet.trade/count.by`,
name: `钱包交易分组计数`,
post: async function (data = {}, config = {}) {
return await http.post(this.url, data, config)
},
},
/**
* 创建钱包交易
*/
create: {
url: `${config.API_URL}/api/sys/wallet.trade/create`,
name: `创建钱包交易`,
post: async function (data = {}, config = {}) {
return await http.post(this.url, data, config)
},
},
/**
* 删除钱包交易
*/
delete: {
url: `${config.API_URL}/api/sys/wallet.trade/delete`,
name: `删除钱包交易`,
post: async function (data = {}, config = {}) {
return await http.post(this.url, data, config)
},
},
/**
* 编辑钱包交易
*/
edit: {
url: `${config.API_URL}/api/sys/wallet.trade/edit`,
name: `编辑钱包交易`,
post: async function (data = {}, config = {}) {
return await http.post(this.url, data, config)
},
},
/**
* 获取单个钱包交易
*/
get: {
url: `${config.API_URL}/api/sys/wallet.trade/get`,
name: `获取单个钱包交易`,
post: async function (data = {}, config = {}) {
return await http.post(this.url, data, config)
},
},
/**
* 分页查询钱包交易
*/
pagedQuery: {
url: `${config.API_URL}/api/sys/wallet.trade/paged.query`,
name: `分页查询钱包交易`,
post: async function (data = {}, config = {}) {
return await http.post(this.url, data, config)
},
},
}

View File

@ -60,6 +60,9 @@ export default {
hasPermission: function (p) {
return this.permissions.includes('*/*/*') || this.permissions.some((a) => a === p)
},
hasApiPermission: function (p) {
return this.apiPermissions.includes('*/*/*') || this.apiPermissions.some((a) => a === p)
},
}
app.use(JsonViewer)

View File

@ -6,7 +6,7 @@
<el-button
@click="
() => {
this.$router.push({ path: '/sys/job', query: { view: 'fail' } })
this.$router.push({ path: '/system/job', query: { view: 'fail' } })
this.$emit('closed')
}
"
@ -22,7 +22,7 @@
<el-button
@click="
() => {
this.$router.push({ path: '/sys/job' })
this.$router.push({ path: '/system/job' })
this.$emit('closed')
}
"

View File

@ -624,6 +624,6 @@ export default {
链接: 'Link',
框架: 'IFrame',
按钮: 'Button',
倒序排序:'Sort-Descending',
顺序排序:'Sort-Ascending',
倒序排序: 'Sort-Descending',
顺序排序: 'Sort-Ascending',
}

View File

@ -622,6 +622,6 @@ export default {
链接: '链接',
框架: '框架',
按钮: '按钮',
倒序排序:'倒序排序',
顺序排序:'顺序排序',
倒序排序: '倒序排序',
顺序排序: '顺序排序',
}

View File

@ -51,5 +51,12 @@ export default {
global.user?.roles.findIndex((x) => x.ignorePermissionControl) >= 0
? ['*/*/*']
: tool.recursiveFindProperty(preloads[0]?.data, 'type', 'button').map((x) => x.tag)
global.apiPermissions =
global.user?.roles.findIndex((x) => x.ignorePermissionControl) >= 0
? ['*/*/*']
: preloads[1]?.data?.roles
?.map((x) => x.apiIds.join(','))
?.join(',')
.split(',')
},
}

View File

@ -1,6 +1,6 @@
<template>
<el-card :header="$t('登录日志')" shadow="never">
<login-log :keywords="$GLOBAL.user.id" :show-filter="false"></login-log>
<login-log :ownerId="$GLOBAL.user.id.toString()" :show-filter="false"></login-log>
</el-card>
</template>

View File

@ -74,10 +74,19 @@
<na-search
:controls="[
{
type: 'input',
field: ['root', 'keywords'],
placeholder: $t('日志编号 / 登录名 / 客户端IP'),
type: 'select-input',
field: [
'dy',
[
{ label: $t('日志编号'), key: 'id' },
{ label: $t('用户编号'), key: 'owner.id' },
{ label: $t('登录名'), key: 'loginUserName' },
{ label: $t('客户端IP'), key: 'createdClientIp' },
],
],
placeholder: $t('匹配内容'),
style: 'width:25rem',
selectStyle: 'width:8rem',
},
]"
:vue="this"
@ -150,7 +159,15 @@ export default {
naInfo,
},
computed: {},
created() {},
created() {
if (this.ownerId) {
this.query.dynamicFilter.filters.push({
field: 'owner.id',
operator: 'eq',
value: this.ownerId,
})
}
},
data() {
return {
statistics: {
@ -227,8 +244,11 @@ export default {
this.$refs.search.search()
},
onReset() {
if (!this.showFilter) return
Object.entries(this.$refs.selectFilter.selected).forEach(([key, _]) => (this.$refs.selectFilter.selected[key] = ['']))
if (this.showFilter) {
Object.entries(this.$refs.selectFilter.selected).forEach(([key, _]) => (this.$refs.selectFilter.selected[key] = ['']))
}
this.$refs.search.selectInputKey = 'id'
if (this.ownerId) this.$refs.search.selectInputKey = 'owner.id'
},
//搜索
async onSearch(form) {
@ -255,6 +275,13 @@ export default {
)
}
if (typeof form.dy.id === 'string' && form.dy.id.trim() !== '') {
this.query.dynamicFilter.filters.push({
field: 'id',
operator: 'eq',
value: form.dy.id,
})
}
if (typeof form.dy.errorCode === 'string' && form.dy.errorCode.trim() !== '') {
this.query.dynamicFilter.filters.push({
field: 'errorCode',
@ -279,6 +306,15 @@ export default {
})
}
if (typeof form.dy['owner.id'] === 'string' && form.dy['owner.id'].trim() !== '') {
this.$refs.search.selectInputKey = 'owner.id'
this.query.dynamicFilter.filters.push({
field: 'owner.id',
operator: 'eq',
value: form.dy['owner.id'],
})
}
await this.$refs.table.upData()
},
@ -295,6 +331,15 @@ export default {
},
},
async mounted() {
if (this.ownerId) {
this.$refs.search.selectInputKey = 'owner.id'
this.$refs.search.form.dy['owner.id'] = this.ownerId
this.$refs.search.keeps.push({
field: 'owner.id',
value: this.ownerId,
type: 'dy',
})
}
if (this.keywords) {
this.$refs.search.form.root.keywords = this.keywords
this.$refs.search.keeps.push({
@ -303,8 +348,9 @@ export default {
type: 'root',
})
}
this.onReset()
},
props: { keywords: { type: String }, showFilter: { type: Boolean, default: true } },
props: { keywords: { type: String }, showFilter: { type: Boolean, default: true }, ownerId: { type: String } },
watch: {},
}
</script>

View File

@ -67,7 +67,7 @@
config: { props: { label: 'userName', value: 'id' } },
placeholder: '用户',
style: 'width:15rem',
condition: () => $GLOBAL.hasPermission('sys/log/operation/user'),
condition: () => $GLOBAL.hasApiPermission('api/sys/user/query'),
},
{
multiple: true,

View File

@ -0,0 +1,323 @@
<template>
<el-container>
<el-header v-loading="statistics.total === '...'" class="el-header-statistics">
<el-row :gutter="15">
<el-col :lg="24">
<el-card shadow="never">
<scStatistic :title="$t('总数')" :value="statistics.total" group-separator></scStatistic>
</el-card>
</el-col>
</el-row>
</el-header>
<el-header class="el-header-select-filter">
<scSelectFilter
:data="[
{
title: $t('交易方向'),
key: 'tradeDirection',
options: [
{ label: '全部', value: '' },
...Object.entries(this.$GLOBAL.enums.tradeDirections).map((x) => {
return {
value: x[0],
label: x[1][1],
badge: this.statistics.tradeDirection?.find((y) => y.key.tradeDirection.toLowerCase() === x[0].toLowerCase())
?.value,
}
}),
],
},
{
title: $t('交易类型'),
key: 'tradeType',
options: [
{ label: '全部', value: '' },
...Object.entries(this.$GLOBAL.enums.tradeTypes).map((x) => {
return {
value: x[0],
label: x[1][1],
badge: this.statistics.tradeType?.find((y) => y.key.tradeType.toLowerCase() === x[0].toLowerCase())?.value,
}
}),
],
},
]"
:label-width="15"
@on-change="filterChange"
ref="selectFilter"></scSelectFilter>
</el-header>
<el-header>
<div class="left-panel">
<na-search
:controls="[
{
type: 'select-input',
field: [
'dy',
[
{ label: $t('交易编号'), key: 'id' },
{ label: $t('用户名'), key: 'owner.userName' },
{ label: $t('用户编号'), key: 'ownerId' },
],
],
placeholder: $t('匹配内容'),
style: 'width:25rem',
selectStyle: 'width:8rem',
},
]"
:vue="this"
@reset="onReset"
@search="onSearch"
dateFormat="YYYY-MM-DD HH:mm:ss"
dateType="datetimerange"
dateValueFormat="YYYY-MM-DD HH:mm:ss"
ref="search" />
</div>
<div class="right-panel"></div>
</el-header>
<el-main class="nopadding">
<scTable
:context-menus="[
'id',
'ownerId',
'createdTime',
'tradeType',
'amount',
'balanceBefore',
'summary',
'owner.userName',
'tradeDirection',
]"
:context-multi="{ id: ['createdTime'], ownerId: ['owner.userName'] }"
:context-opers="['view']"
:default-sort="{ prop: 'id', order: 'descending' }"
:export-api="$API.sys_wallettrade.export"
:params="query"
:query-api="$API.sys_wallettrade.pagedQuery"
:vue="this"
@data-change="getStatistics"
@selection-change="
(items) => {
selection = items
}
"
ref="table"
remote-filter
remote-sort
row-key="id"
stripe>
<naColId :label="$t('交易编号')" prop="id" sortable="custom" width="170" />
<naColUser
:clickOpenDialog="$GLOBAL.hasApiPermission('api/sys/user/get')"
:label="$t('所属用户')"
header-align="center"
nestProp="owner.userName"
nestProp2="ownerId"
prop="ownerId"
sortable="custom"
width="170"></naColUser>
<naColIndicator
:label="$t('交易方向')"
:options="
Object.entries(this.$GLOBAL.enums.tradeDirections).map((x) => {
return { value: x[0], text: `${x[1][1]}`, type: x[1][2], pulse: x[1][3] === 'true' }
})
"
align="center"
prop="tradeDirection"
sortable="custom" />
<naColIndicator
:label="$t('交易类型')"
:options="
Object.entries(this.$GLOBAL.enums.tradeTypes).map((x) => {
return { value: x[0], text: `${x[1][1]}`, type: x[1][2], pulse: x[1][3] === 'true' }
})
"
align="center"
prop="tradeType"
sortable="custom" />
<el-table-column
:formatter="(row) => $TOOL.groupSeparator(row.balanceBefore)"
:label="$t('交易前余额')"
align="right"
prop="balanceBefore"
sortable="custom" />
<el-table-column
:formatter="(row) => $TOOL.groupSeparator(row.amount)"
:label="$t('发生金额')"
align="right"
prop="amount"
sortable="custom" />
<el-table-column :label="$t('交易后余额')" align="right">
<template #default="{ row }">
{{ $TOOL.groupSeparator(row.balanceBefore + row.amount) }}
</template>
</el-table-column>
<el-table-column :label="$t('备注')" min-width="100" prop="summary" show-overflow-tooltip sortable="custom" />
<naColOperation :buttons="[naColOperation.buttons[0]]" :vue="this" width="50" />
</scTable>
</el-main>
</el-container>
<save-dialog
v-if="dialog.save"
@closed="dialog.save = null"
@mounted="$refs.saveDialog.open(dialog.save)"
@success="(data, mode) => $refs.table.refresh()"
ref="saveDialog"></save-dialog>
</template>
<script>
import { defineAsyncComponent } from 'vue'
import table from '@/config/table'
import naColOperation from '@/config/naColOperation'
const naColUser = defineAsyncComponent(() => import('@/components/naColUser'))
const saveDialog = defineAsyncComponent(() => import('./save.vue'))
export default {
components: {
naColUser,
saveDialog,
},
computed: {
naColOperation() {
return naColOperation
},
table() {
return table
},
},
async created() {
if (this.ownerId) {
this.query.dynamicFilter.filters.push({ field: 'ownerId', operator: 'eq', value: this.ownerId })
}
},
data() {
return {
statistics: {
total: '...',
},
dialog: {},
loading: false,
query: {
dynamicFilter: {
filters: [],
},
filter: {},
keywords: this.keywords,
},
selection: [],
}
},
inject: ['reload'],
methods: {
filterChange(data) {
Object.entries(data).forEach(([key, value]) => {
this.$refs.search.form.dy[key] = value === 'true' ? true : value === 'false' ? false : value
})
this.$refs.search.search()
},
async getStatistics() {
this.statistics.total = this.$refs.table?.total
const res = await Promise.all([
this.$API.sys_wallettrade.countBy.post({
dynamicFilter: {
filters: this.query.dynamicFilter.filters,
},
requiredFields: ['TradeDirection'],
}),
this.$API.sys_wallettrade.countBy.post({
dynamicFilter: {
filters: this.query.dynamicFilter.filters,
},
requiredFields: ['TradeType'],
}),
])
this.statistics.tradeDirection = res[0].data
this.statistics.tradeType = res[1].data
},
//重置
onReset() {
Object.entries(this.$refs.selectFilter.selected).forEach(([key, _]) => (this.$refs.selectFilter.selected[key] = ['']))
if (this.ownerId) {
this.$refs.search.selectInputKey = 'ownerId'
}
},
//搜索
async onSearch(form) {
if (Array.isArray(form.dy.createdTime)) {
this.query.dynamicFilter.filters.push({
field: 'createdTime',
operator: 'dateRange',
value: form.dy.createdTime.map((x) => x.replace(/ 00:00:00$/, '')),
})
}
if (typeof form.dy['owner.userName'] === 'string' && form.dy['owner.userName'].trim() !== '') {
this.query.dynamicFilter.filters.push({
field: 'owner.userName',
operator: 'eq',
value: form.dy['owner.userName'],
})
}
if (typeof form.dy['ownerId'] === 'string' && form.dy['ownerId'].trim() !== '') {
this.query.dynamicFilter.filters.push({
field: 'ownerId',
operator: 'eq',
value: form.dy['ownerId'],
})
}
if (typeof form.dy['id'] === 'string' && form.dy['id'].trim() !== '') {
this.query.dynamicFilter.filters.push({
field: 'id',
operator: 'eq',
value: form.dy['id'],
})
}
if (typeof form.dy['tradeType'] === 'string' && form.dy['tradeType'].trim() !== '') {
this.query.dynamicFilter.filters.push({
field: 'tradeType',
operator: 'eq',
value: form.dy['tradeType'],
})
}
if (typeof form.dy['tradeDirection'] === 'string' && form.dy['tradeDirection'].trim() !== '') {
this.query.dynamicFilter.filters.push({
field: 'tradeDirection',
operator: 'eq',
value: form.dy['tradeDirection'],
})
}
await this.$refs.table.upData()
},
},
async mounted() {
if (this.ownerId) {
this.$refs.search.selectInputKey = 'ownerId'
this.$refs.search.form.dy.ownerId = this.ownerId
this.$refs.search.keeps.push({
field: 'ownerId',
value: this.ownerId,
type: 'dy',
})
}
if (this.keywords) {
this.$refs.search.form.root.keywords = this.keywords
this.$refs.search.keeps.push({
field: 'keywords',
value: this.keywords,
type: 'root',
})
}
this.onReset()
},
props: ['keywords', 'ownerId'],
watch: {},
}
</script>
<style scoped></style>

View File

@ -0,0 +1,119 @@
<template>
<scDialog v-model="visible" :title="`${titleMap[mode]}${form?.id ?? '...'}`" @closed="$emit('closed')" destroy-on-close full-screen>
<div v-loading="loading">
<el-tabs v-model="tabId" @tab-change="tabChange" tab-position="top">
<el-tab-pane :label="$t('基本信息')" name="basic">
<el-form :disabled="mode === 'view'" :model="form" :rules="rules" label-width="15rem" ref="dialogForm">
<el-form-item :label="$t('唯一编码')" prop="id">
<el-input v-model="form.id" clearable />
</el-form-item>
<el-form-item :label="$t('交易方向')" prop="tradeDirection">
<el-input v-model="form.tradeDirection" clearable />
</el-form-item>
<el-form-item :label="$t('交易类型')" prop="tradeType">
<el-input v-model="form.tradeType" clearable />
</el-form-item>
<el-form-item :label="$t('交易前余额')" prop="balanceBefore">
<el-input v-model="form.balanceBefore" clearable />
</el-form-item>
<el-form-item :label="$t('交易金额')" prop="amount">
<el-input v-model="form.amount" clearable />
</el-form-item>
<el-form-item :label="$t('交易后余额')">
<el-input :value="form.balanceBefore + form.amount" clearable />
</el-form-item>
<el-form-item :label="$t('所有者部门编号')" prop="ownerDeptId">
<el-input v-model="form.ownerDeptId" clearable />
</el-form-item>
<el-form-item :label="$t('所有者用户编号')" prop="ownerId">
<el-input v-model="form.ownerId" clearable />
</el-form-item>
<el-form-item :label="$t('创建者用户编号')" prop="createdUserId">
<el-input v-model="form.createdUserId" clearable />
</el-form-item>
<el-form-item :label="$t('所有者用户名')" prop="createdUserName">
<el-input v-model="form.createdUserName" clearable />
</el-form-item>
<el-form-item :label="$t('创建时间')" prop="createdTime">
<el-input v-model="form.createdTime" clearable />
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane v-if="mode === 'view'" :label="$t('原始数据')">
<JsonViewer
:expand-depth="5"
:theme="this.$TOOL.data.get('APP_SET_DARK') || this.$CONFIG.APP_SET_DARK ? 'dark' : 'light'"
:value="form"
copyable
expanded
sort></JsonViewer>
</el-tab-pane>
</el-tabs>
</div>
<template #footer>
<el-button @click="visible = false">{{ $t('取消') }}</el-button>
<el-button v-if="mode !== 'view'" :disabled="loading" :loading="loading" @click="submit" type="primary">{{ $t('保存') }}</el-button>
</template>
</scDialog>
</template>
<script>
export default {
components: {},
data() {
return {
//表单数据
form: {},
loading: true,
mode: 'add',
//验证规则
rules: {},
tabId: 'basic',
titleMap: {
add: this.$t('新增交易'),
edit: this.$t('编辑交易'),
view: this.$t('查看交易'),
},
visible: false,
}
},
emits: ['success', 'closed', 'mounted'],
methods: {
//显示
async open(data) {
this.visible = true
if (data.mode === 'add') {
this.loading = false
return this
}
this.loading = true
this.mode = data.mode
if (data.row?.id) {
const res = await this.$API.sys_wallettrade.get.post({ id: data.row.id })
if (res.data) {
Object.assign(this.form, res.data)
this.loading = false
return this
}
}
this.$message.error(`未找到该数据`)
return this
},
//表单提交方法
async submit() {
const valid = await this.$refs.dialogForm.validate().catch(() => {})
if (!valid) {
return false
}
this.loading = true
//
this.loading = false
},
},
mounted() {
this.$emit('mounted')
},
}
</script>
<style scoped></style>

View File

@ -116,8 +116,8 @@
<naColAvatar :label="$t('用户名')" prop="userName" width="170" />
<el-table-column :label="$t('手机号 / 邮箱')" align="right" prop="mobile" sortable="custom" width="250">
<template #default="{ row }">
<p>{{ row.mobile }}</p>
<p>{{ row.email }}</p>
<p>{{ row.mobile ?? '-' }}</p>
<p>{{ row.email ?? '-' }}</p>
</template>
</el-table-column>
<naColTags

View File

@ -7,7 +7,7 @@
destroy-on-close
full-screen>
<el-form
:disabled="mode === 'view' && tabId !== 'log'"
:disabled="mode === 'view' && tabId !== 'log' && tabId !== 'wallet' && tabId !== 'trade'"
:model="form"
:rules="rules"
label-position="right"
@ -234,6 +234,12 @@
</el-col>
</el-row>
</el-tab-pane>
<el-tab-pane v-if="mode === 'view'" :label="$t('钱包信息')" name="wallet">
<wallet v-if="tabId === 'wallet'" :id="form.id.toString()" />
</el-tab-pane>
<el-tab-pane v-if="mode === 'view'" :label="$t('交易流水')" name="trade">
<trade v-if="tabId === 'trade'" :ownerId="form.id.toString()" />
</el-tab-pane>
<el-tab-pane v-if="mode === 'view'" :label="$t('操作日志')" name="log">
<log v-if="tabId === 'log'" :owner-id="form.id"></log>
</el-tab-pane>
@ -259,12 +265,14 @@
import { defineAsyncComponent } from 'vue'
const log = defineAsyncComponent(() => import('@/views/sys/log/operation'))
const trade = defineAsyncComponent(() => import('@/views/sys/trade'))
const wallet = defineAsyncComponent(() => import('@/views/sys/wallet'))
const naArea = defineAsyncComponent(() => import('@/components/naArea'))
const naDept = defineAsyncComponent(() => import('@/components/naDept'))
const scUpload = defineAsyncComponent(() => import('@/components/scUpload'))
const scSelect = defineAsyncComponent(() => import('@/components/scSelect'))
export default {
components: { log, naArea, naDept, scUpload, scSelect },
components: { log, naArea, naDept, scUpload, scSelect, trade, wallet },
data() {
return {
//表单数据

View File

@ -0,0 +1,315 @@
<template>
<el-container>
<el-header v-loading="statistics.total === '...'" class="el-header-statistics">
<el-row :gutter="15">
<el-col :lg="24">
<el-card shadow="never">
<scStatistic :title="$t('总数')" :value="statistics.total" group-separator></scStatistic>
</el-card>
</el-col>
</el-row>
</el-header>
<el-header>
<div class="left-panel">
<na-search
:controls="[
{
type: 'select-input',
field: [
'dy',
[
{ label: $t('用户编号'), key: 'id' },
{ label: $t('用户名'), key: 'owner.userName' },
{ label: $t('电子邮箱'), key: 'owner.email' },
{ label: $t('手机号'), key: 'owner.mobile' },
],
],
placeholder: $t('匹配内容'),
style: 'width:25rem',
selectStyle: 'width:8rem',
},
{
type: 'remote-select',
field: ['filter', 'deptId'],
api: $API.sys_dept.query,
config: { props: { label: 'name', value: 'id' } },
placeholder: $t('所属部门'),
style: 'width:15rem',
condition: () => $GLOBAL.hasApiPermission('api/sys/dept/query'),
},
{
type: 'remote-select',
field: ['filter', 'roleId'],
api: $API.sys_role.query,
config: { props: { label: 'name', value: 'id' } },
placeholder: $t('所属角色'),
style: 'width:15rem',
condition: () => $GLOBAL.hasApiPermission('api/sys/dept/query'),
},
]"
:vue="this"
@reset="onReset"
@search="onSearch"
dateFormat="YYYY-MM-DD HH:mm:ss"
dateType="datetimerange"
dateValueFormat="YYYY-MM-DD HH:mm:ss"
ref="search" />
</div>
<div class="right-panel"></div>
</el-header>
<el-main class="nopadding">
<scTable
:context-menus="[
'id',
'ownerId',
'owner.userName',
'createdTime',
'totalBalance',
'availableBalance',
'frozenBalance',
'totalIncome',
'totalExpenditure',
'modifiedTime',
]"
:context-multi="{ id: ['createdTime'], ownerId: ['owner.userName'] }"
:context-opers="['view']"
:default-sort="{ prop: 'id', order: 'descending' }"
:export-api="$API.sys_userwallet.export"
:params="query"
:query-api="$API.sys_userwallet.pagedQuery"
:vue="this"
@data-change="getStatistics"
@selection-change="
(items) => {
selection = items
}
"
ref="table"
remote-filter
remote-sort
row-key="id"
stripe>
<naColId :label="$t('钱包编号')" prop="id" sortable="custom" width="170" />
<naColUser
:clickOpenDialog="$GLOBAL.hasApiPermission('api/sys/user/get')"
:label="$t('所属用户')"
nestProp="owner.userName"
nestProp2="ownerId"
prop="ownerId"
sortable="custom"
width="170"></naColUser>
<el-table-column
:formatter="(row) => $TOOL.groupSeparator(row.totalBalance)"
:label="$t('总余额')"
align="right"
prop="totalBalance"
sortable="custom" />
<el-table-column
:formatter="(row) => $TOOL.groupSeparator(row.availableBalance)"
:label="$t('可用余额')"
align="right"
prop="availableBalance"
sortable="custom" />
<el-table-column
:formatter="(row) => $TOOL.groupSeparator(row.frozenBalance)"
:label="$t('冻结余额')"
align="right"
prop="frozenBalance"
sortable="custom" />
<el-table-column
:formatter="(row) => $TOOL.groupSeparator(row.totalIncome)"
:label="$t('总收入')"
align="right"
prop="totalIncome"
sortable="custom" />
<el-table-column
:formatter="(row) => $TOOL.groupSeparator(row.totalExpenditure)"
:label="$t('总支出')"
align="right"
prop="totalExpenditure"
sortable="custom" />
<el-table-column v-tim :label="$t('最后交易时间')" align="right" prop="modifiedTime" sortable="custom" width="150">
<template #default="{ row }">
<span v-if="row.modifiedTime" v-time.tip="row.modifiedTime" :title="row.modifiedTime"></span>
</template>
</el-table-column>
<naColOperation
:buttons="[
naColOperation.buttons[0],
{
icon: 'el-icon-plus',
title: $t('新建交易'),
click: async (row, vue) => {
vue.dialog.trade = { row }
},
condition: () => {
return $GLOBAL.hasApiPermission('api/sys/wallet.trade/create')
},
},
]"
:vue="this"
width="120" />
</scTable>
</el-main>
</el-container>
<trade-dialog
v-if="dialog.trade"
@closed="dialog.trade = null"
@mounted="$refs.tradeDialog.open(dialog.trade)"
@success="(data, mode) => $refs.table.refresh()"
ref="tradeDialog"></trade-dialog>
<save-dialog
v-if="dialog.save"
@closed="dialog.save = null"
@mounted="$refs.saveDialog.open(dialog.save)"
@success="(data, mode) => $refs.table.refresh()"
ref="saveDialog"></save-dialog>
</template>
<script>
import { defineAsyncComponent } from 'vue'
import table from '@/config/table'
import naColOperation from '@/config/naColOperation'
const tradeDialog = defineAsyncComponent(() => import('./trade.vue'))
const saveDialog = defineAsyncComponent(() => import('./save.vue'))
const naColUser = defineAsyncComponent(() => import('@/components/naColUser'))
export default {
components: {
tradeDialog,
saveDialog,
naColUser,
},
computed: {
naColOperation() {
return naColOperation
},
table() {
return table
},
},
async created() {
if (this.roleId) {
this.query.filter.roleId = this.roleId
}
if (this.deptId) {
this.query.filter.deptId = this.deptId
}
if (this.id) {
this.query.dynamicFilter.filters.push({ field: 'id', operator: 'eq', value: this.id })
}
},
data() {
return {
statistics: {
total: '...',
},
dialog: {},
loading: false,
query: {
dynamicFilter: {
filters: [],
},
filter: {},
keywords: this.keywords,
},
selection: [],
}
},
inject: ['reload'],
methods: {
async getStatistics() {
this.statistics.total = this.$refs.table?.total
},
//重置
onReset() {
if (this.id) {
this.$refs.search.selectInputKey = 'id'
}
},
//搜索
async onSearch(form) {
if (Array.isArray(form.dy.createdTime)) {
this.query.dynamicFilter.filters.push({
field: 'createdTime',
operator: 'dateRange',
value: form.dy.createdTime.map((x) => x.replace(/ 00:00:00$/, '')),
})
}
if (typeof form.dy['owner.userName'] === 'string' && form.dy['owner.userName'].trim() !== '') {
this.query.dynamicFilter.filters.push({
field: 'owner.userName',
operator: 'eq',
value: form.dy['owner.userName'],
})
}
if (typeof form.dy['owner.email'] === 'string' && form.dy['owner.email'].trim() !== '') {
this.query.dynamicFilter.filters.push({
field: 'owner.email',
operator: 'eq',
value: form.dy['owner.email'],
})
}
if (typeof form.dy['owner.mobile'] === 'string' && form.dy['owner.mobile'].trim() !== '') {
this.query.dynamicFilter.filters.push({
field: 'owner.mobile',
operator: 'eq',
value: form.dy['owner.mobile'],
})
}
if (typeof form.dy['id'] === 'string' && form.dy['id'].trim() !== '') {
this.query.dynamicFilter.filters.push({
field: 'id',
operator: 'eq',
value: form.dy['id'],
})
}
await this.$refs.table.upData()
},
},
async mounted() {
if (this.keywords) {
this.$refs.search.form.root.keywords = this.keywords
this.$refs.search.keeps.push({
field: 'keywords',
value: this.keywords,
type: 'root',
})
}
if (this.id) {
this.$refs.search.selectInputKey = 'id'
this.$refs.search.form.dy.id = this.id
this.$refs.search.keeps.push({
field: 'id',
value: this.id,
type: 'dy',
})
}
if (this.roleId) {
this.$refs.search.form.filter.roleId = this.roleId
this.$refs.search.keeps.push({
field: 'roleId',
value: this.roleId,
type: 'filter',
})
}
if (this.deptId) {
this.$refs.search.form.filter.deptId = this.deptId
this.$refs.search.keeps.push({
field: 'deptId',
value: this.deptId,
type: 'filter',
})
}
this.onReset()
},
props: ['keywords', 'roleId', 'deptId', 'id'],
watch: {},
}
</script>
<style scoped></style>

View File

@ -0,0 +1,124 @@
<template>
<scDialog v-model="visible" :title="`${titleMap[mode]}${form?.id ?? '...'}`" @closed="$emit('closed')" destroy-on-close full-screen>
<div v-loading="loading">
<el-tabs v-model="tabId" @tab-change="tabChange" tab-position="top">
<el-tab-pane :label="$t('基本信息')" name="basic">
<el-form :disabled="mode === 'view'" :model="form" :rules="rules" label-width="15rem" ref="dialogForm">
<el-form-item :label="$t('唯一编码')" prop="id">
<el-input v-model="form.id" clearable />
</el-form-item>
<el-form-item :label="$t('总余额')" prop="totalBalance">
<el-input v-model="form.totalBalance" clearable />
</el-form-item>
<el-form-item :label="$t('可用余额')" prop="availableBalance">
<el-input v-model="form.availableBalance" clearable />
</el-form-item>
<el-form-item :label="$t('冻结余额')" prop="frozenBalance">
<el-input v-model="form.frozenBalance" clearable />
</el-form-item>
<el-form-item :label="$t('总收入')" prop="totalIncome">
<el-input v-model="form.totalIncome" clearable />
</el-form-item>
<el-form-item :label="$t('总支出')" prop="totalExpenditure">
<el-input v-model="form.totalExpenditure" clearable />
</el-form-item>
<el-form-item :label="$t('所有者部门编号')" prop="ownerDeptId">
<el-input v-model="form.ownerDeptId" clearable />
</el-form-item>
<el-form-item :label="$t('所有者用户编号')" prop="ownerId">
<el-input v-model="form.ownerId" clearable />
</el-form-item>
<el-form-item :label="$t('创建时间')" prop="createdTime">
<el-input v-model="form.createdTime" clearable />
</el-form-item>
<el-form-item :label="$t('修改时间')" prop="modifiedTime">
<el-input v-model="form.modifiedTime" clearable />
</el-form-item>
<el-form-item :label="$t('数据版本')" prop="version">
<el-input v-model="form.version" :disabled="mode === 'edit'" clearable />
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane v-if="mode === 'view'" :label="$t('交易流水')" name="trade">
<trade v-if="tabId === 'trade'" :ownerId="form.ownerId.toString()" />
</el-tab-pane>
<el-tab-pane v-if="mode === 'view'" :label="$t('原始数据')">
<JsonViewer
:expand-depth="5"
:theme="this.$TOOL.data.get('APP_SET_DARK') || this.$CONFIG.APP_SET_DARK ? 'dark' : 'light'"
:value="form"
copyable
expanded
sort></JsonViewer>
</el-tab-pane>
</el-tabs>
</div>
<template #footer>
<el-button @click="visible = false">{{ $t('取消') }}</el-button>
<el-button v-if="mode !== 'view'" :disabled="loading" :loading="loading" @click="submit" type="primary">{{ $t('保存') }}</el-button>
</template>
</scDialog>
</template>
<script>
import { defineAsyncComponent } from 'vue'
const trade = defineAsyncComponent(() => import('@/views/sys/trade'))
export default {
components: { trade },
data() {
return {
//表单数据
form: {},
loading: true,
mode: 'add',
//验证规则
rules: {},
tabId: 'basic',
titleMap: {
add: this.$t('新增钱包'),
edit: this.$t('编辑钱包'),
view: this.$t('查看钱包'),
},
visible: false,
}
},
emits: ['success', 'closed', 'mounted'],
methods: {
//显示
async open(data) {
this.visible = true
if (data.mode === 'add') {
this.loading = false
return this
}
this.loading = true
this.mode = data.mode
if (data.row?.id) {
const res = await this.$API.sys_userwallet.get.post({ id: data.row.id })
if (res.data) {
Object.assign(this.form, res.data)
this.loading = false
return this
}
}
this.$message.error(`未找到该数据`)
return this
},
//表单提交方法
async submit() {
const valid = await this.$refs.dialogForm.validate().catch(() => {})
if (!valid) {
return false
}
this.loading = true
//
this.loading = false
},
},
mounted() {
this.$emit('mounted')
},
}
</script>
<style scoped></style>

View File

@ -0,0 +1,91 @@
<template>
<scDialog v-model="visible" :title="$t('新建交易')" @closed="$emit('closed')" append-to-body destroy-on-close>
<el-form :model="form" :rules="rules" label-position="right" label-width="12rem" ref="dialogForm" style="height: 100%">
<el-form-item>
<el-descriptions border column="1">
<el-descriptions-item :label="$t('用户名')">
<b>{{ row.owner.userName }}</b>
</el-descriptions-item>
<el-descriptions-item :label="$t('用户编号')">{{ row.owner.id }}</el-descriptions-item>
<el-descriptions-item :label="$t('可用余额')">{{ $TOOL.groupSeparator(row.availableBalance) }}</el-descriptions-item>
</el-descriptions>
</el-form-item>
<el-form-item :label="$t('交易类型')" prop="tradeType">
<el-select v-model="form.tradeType" clearable filterable>
<el-option v-for="(item, i) in $GLOBAL.enums.tradeTypes" :key="i" :label="item[1]" :value="i" />
</el-select>
</el-form-item>
<el-form-item :label="$t('交易金额')" prop="amount">
<el-input-number v-model="form.amount" :max="999999999" :min="-999999999" precision="0" style="width: 15rem"></el-input-number>
</el-form-item>
<el-form-item :label="$t('备注')" prop="summary">
<el-input v-model="form.summary" rows="3" type="textarea" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="visible = false">{{ $t('取消') }}</el-button>
<el-button v-if="mode !== 'view'" :disabled="loading" :loading="loading" @click="submit" type="primary">{{ $t('保存') }}</el-button>
</template>
</scDialog>
</template>
<script>
import { defineAsyncComponent } from 'vue'
export default {
components: {},
data() {
return {
//表单数据
form: {},
row: {},
loading: true,
//验证规则
rules: {
tradeType: [{ required: true, message: this.$t('请选择交易类型') }],
amount: [{ required: true, message: this.$t('请输入交易金额') }],
},
tabId: '0',
titleMap: {
add: this.$t('新增用户'),
edit: this.$t('编辑用户'),
view: this.$t('查看用户'),
},
visible: false,
}
},
emits: ['success', 'closed', 'mounted'],
methods: {
//显示
async open(data) {
this.row = data.row
this.form.ownerId = data.row.id
this.visible = true
this.loading = false
return this
},
//表单提交方法
async submit() {
const valid = await this.$refs.dialogForm.validate().catch(() => {})
if (!valid) {
return false
}
this.loading = true
try {
const res = await this.$API.sys_wallettrade.create.post(this.form)
this.$emit('success', res.data, this.mode)
this.visible = false
this.$message.success(this.$t('操作成功'))
} catch {}
this.loading = false
},
},
mounted() {
this.$emit('mounted')
},
}
</script>
<style scoped></style>