BUG修复,及部分页面增加按钮权限

This commit is contained in:
Songsl 2026-04-28 22:29:59 +08:00
parent 2f54ebef78
commit 6a714a77a9
14 changed files with 323 additions and 195 deletions

View File

@ -1,58 +1,65 @@
<template>
<el-dialog :model-value="visible" :title="title" :width="width" :destroy-on-close="destroyOnClose"
:fullscreen="fullscreen"
@close="handleClose" v-bind="$attrs">
<!-- 对话框内容插槽 -->
<slot />
<el-dialog
:model-value="visible"
:title="title"
:width="width"
align-center
:destroy-on-close="destroyOnClose"
:fullscreen="fullscreen"
@close="handleClose"
v-bind="$attrs"
>
<!-- 对话框内容插槽 -->
<slot />
<!-- 底部按钮插槽 -->
<template #footer v-if="showFooter">
<slot name="footer">
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleConfirm">确定</el-button>
</slot>
</template>
</el-dialog>
<!-- 底部按钮插槽 -->
<template #footer v-if="showFooter">
<slot name="footer">
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleConfirm">确定</el-button>
</slot>
</template>
</el-dialog>
</template>
<script setup lang="ts">
interface Props {
visible: boolean
title: string
width?: string | number
destroyOnClose?: boolean
type?: string
showFooter?: boolean
fullscreen?: boolean
visible: boolean
title: string
width?: string | number
destroyOnClose?: boolean
type?: string
showFooter?: boolean
fullscreen?: boolean
}
interface Emits {
(e: 'update:visible', value: boolean): void
(e: 'confirm'): void
(e: 'cancel'): void
(e: 'close'): void
(e: 'update:visible', value: boolean): void
(e: 'confirm'): void
(e: 'cancel'): void
(e: 'close'): void
}
const props = withDefaults(defineProps<Props>(), {
width: '750px',
destroyOnClose: true,
showFooter: true,
fullscreen: false,
width: '750px',
destroyOnClose: true,
showFooter: true,
fullscreen: false,
})
const emit = defineEmits<Emits>()
const handleConfirm = () => {
emit('confirm')
emit('confirm')
}
const handleCancel = () => {
emit('update:visible', false)
emit('cancel')
emit('update:visible', false)
emit('cancel')
}
const handleClose = () => {
emit('close')
emit('close')
}
</script>

View File

@ -1,102 +1,130 @@
<template>
<div class="search-container">
<el-form :model="searchForm" inline @submit.prevent>
<!-- 动态生成的表单项 -->
<template v-for="item in searchOptions" :key="item.prop">
<el-form-item :label="item.label" :prop="item.prop" v-if="!item.slot"
:style="{ minWidth: item.minWidth, width: item.width }">
<!-- 输入框 -->
<el-input v-if="item.type === 'input'" v-model="searchForm[item.prop]"
:placeholder="item.placeholder || `请输入${item.label}`" :clearable="item.clearable ?? true"
:disabled="item.disabled" @blur="handleBlur"/>
<div class="search-container">
<el-form :model="searchForm" inline @submit.prevent>
<!-- 动态生成的表单项 -->
<template v-for="item in searchOptions" :key="item.prop">
<el-form-item
v-auth="item.auth || ''"
:label="item.label"
:prop="item.prop"
v-if="!item.slot"
:style="{
minWidth: item.minWidth,
width: item.width,
flex: item.width ? '0 0 auto' : '',
}"
>
<!-- 输入框 -->
<el-input
v-if="item.type === 'input'"
v-model="searchForm[item.prop]"
:placeholder="item.placeholder || `请输入${item.label}`"
:clearable="item.clearable ?? true"
:disabled="item.disabled"
@blur="handleBlur"
/>
<!-- 选择器 -->
<el-select v-else-if="item.type === 'select'" v-model="searchForm[item.prop]"
:placeholder="item.placeholder || `请选择${item.label}`" :clearable="item.clearable ?? true"
:disabled="item.disabled">
<el-option v-for="option in item.options" :key="option.value" :label="option.label"
:value="option.value" />
</el-select>
<!-- 选择器 -->
<el-select
v-else-if="item.type === 'select'"
v-model="searchForm[item.prop]"
:placeholder="item.placeholder || `请选择${item.label}`"
:clearable="item.clearable ?? true"
:disabled="item.disabled"
>
<el-option
v-for="option in item.options"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
<!-- 日期选择器单个/范围 -->
<el-date-picker v-else-if="item.type === 'date'" v-model="searchForm[item.prop]"
:type="item.dateType || 'date'" :range-separator="item.rangeSeparator || '至'"
:start-placeholder="item.startPlaceholder || '开始日期'"
:end-placeholder="item.endPlaceholder || '结束日期'"
:value-format="item.valueFormat || 'YYYY-MM-DD'" :clearable="item.clearable ?? true"
:disabled-date="item.disabledDate" :locale="'zhCn'" />
</el-form-item>
<!-- 日期选择器单个/范围 -->
<el-date-picker
v-else-if="item.type === 'date'"
v-model="searchForm[item.prop]"
:type="item.dateType || 'date'"
:range-separator="item.rangeSeparator || '至'"
:start-placeholder="item.startPlaceholder || '开始日期'"
:end-placeholder="item.endPlaceholder || '结束日期'"
:value-format="item.valueFormat || 'YYYY-MM-DD'"
:clearable="item.clearable ?? true"
:disabled-date="item.disabledDate"
:locale="'zhCn'"
/>
</el-form-item>
<!-- 自定义插槽项 -->
<el-form-item :label="item.label" :prop="item.prop" v-else>
<slot :name="item.prop" :form="searchForm" />
</el-form-item>
</template>
<!-- 自定义插槽项 -->
<el-form-item :label="item.label" :prop="item.prop" v-else>
<slot :name="item.prop" :form="searchForm" />
</el-form-item>
</template>
<!-- 额外插槽用于插入配置外的自定义项 -->
<slot name="extra" :form="searchForm" />
<!-- 额外插槽用于插入配置外的自定义项 -->
<slot name="extra" :form="searchForm" />
<!-- 操作按钮 -->
<el-form-item>
<el-button type="primary" @click="$emit('search')">
{{ buttonConfig.searchText || '搜索' }}
</el-button>
<el-button @click="$emit('reset')">
{{ buttonConfig.resetText || '重置' }}
</el-button>
<!-- 按钮区插槽 -->
<slot name="button" :form="searchForm" />
</el-form-item>
</el-form>
</div>
<!-- 操作按钮 -->
<el-form-item>
<el-button type="primary" @click="$emit('search')">
{{ buttonConfig.searchText || '搜索' }}
</el-button>
<el-button @click="$emit('reset')">
{{ buttonConfig.resetText || '重置' }}
</el-button>
<!-- 按钮区插槽 -->
<slot name="button" :form="searchForm" />
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
//
interface SearchOption {
prop: string; //
label: string; //
type: 'input' | 'select' | 'date'; //
slot?: boolean; //
placeholder?: string; //
clearable?: boolean; //
disabled?: boolean; //
options?: { label: string; value: any }[]; // //
dateType?: 'date' | 'daterange' | 'datetime' | 'datetimerange'; //
rangeSeparator?: string; //
startPlaceholder?: string; //
endPlaceholder?: string; //
valueFormat?: string; //
disabledDate?: (date: Date) => boolean; //
width?: string; //
minWidth?: string; //
prop: string //
label: string //
type: 'input' | 'select' | 'date' //
slot?: boolean //
placeholder?: string //
clearable?: boolean //
disabled?: boolean //
options?: { label: string; value: any }[] // //
dateType?: 'date' | 'daterange' | 'datetime' | 'datetimerange' //
rangeSeparator?: string //
startPlaceholder?: string //
endPlaceholder?: string //
valueFormat?: string //
disabledDate?: (date: Date) => boolean //
width?: string //
minWidth?: string //
auth?: string //
}
//
interface ButtonConfig {
searchText?: string;
resetText?: string;
searchText?: string
resetText?: string
}
// Props
const props = defineProps({
searchForm: {
type: Object,
required: true,
},
searchOptions: {
type: Array as () => SearchOption[
],
required: true,
},
buttonConfig: {
type: Object as () => ButtonConfig,
default: () => ({}),
}
searchForm: {
type: Object,
required: true,
},
searchOptions: {
type: Array as () => SearchOption[],
required: true,
},
buttonConfig: {
type: Object as () => ButtonConfig,
default: () => ({}),
},
})
//
const emit = defineEmits(['search', 'reset','blur-input'])
const emit = defineEmits(['search', 'reset', 'blur-input'])
// 3. 👇
const handleBlur = () => {
emit('blur-input')
@ -105,24 +133,24 @@ const handleBlur = () => {
<style scoped lang="scss">
.search-container {
margin-bottom: 20px;
margin-bottom: 20px;
.el-form {
display: flex;
width: 100%;
.el-form {
display: flex;
width: 100%;
.el-form-item {
flex: 1 1 auto;
}
.el-form-item {
flex: 1 1 auto;
}
}
:deep(.el-form-item) {
margin-bottom: 0;
margin-right: 16px;
}
:deep(.el-form-item) {
margin-bottom: 0;
margin-right: 16px;
}
:deep(.el-button) {
margin-left: 8px;
}
:deep(.el-button) {
margin-left: 8px;
}
}
</style>

View File

@ -1,17 +1,29 @@
import type { Directive, DirectiveBinding } from 'vue'
import {getStorage} from '@/utils/storage'
import { getStorage } from '@/utils/storage'
/**
* v-auth -
* v-auth -
* v-auth="'admin'" v-auth="'user:read,user:write'"
*/
export const authDirective: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
checkPermission(el, binding.value, true)
checkPermission(el, binding.value, 'some')
},
updated(el: HTMLElement, binding: DirectiveBinding) {
checkPermission(el, binding.value, true)
checkPermission(el, binding.value, 'some')
},
}
/**
* v-auth-all -
* v-auth-all="'user:read,user:write'"
*/
export const authAllDirective: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
checkPermission(el, binding.value, 'every')
},
updated(el: HTMLElement, binding: DirectiveBinding) {
checkPermission(el, binding.value, 'every')
},
}
@ -21,10 +33,10 @@ export const authDirective: Directive = {
*/
export const authNotDirective: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
checkPermission(el, binding.value, false)
checkPermission(el, binding.value, 'none')
},
updated(el: HTMLElement, binding: DirectiveBinding) {
checkPermission(el, binding.value, false)
checkPermission(el, binding.value, 'none')
},
}
@ -32,28 +44,64 @@ export const authNotDirective: Directive = {
*
* @param el DOM
* @param value
* @param shouldContain true=false=
* @param mode 'some'=, 'every'=, 'none'=
*/
function checkPermission(el: HTMLElement, value: string | string[], shouldContain: boolean) {
if (!value) {
shouldContain ? el.remove() : (el.style.display = '')
function checkPermission(
el: HTMLElement,
value: string | string[],
mode: 'some' | 'every' | 'none',
) {
// 空字符串/空数组时some/every 模式展示none 模式隐藏
if (!value || (Array.isArray(value) && value.length === 0)) {
if (mode === 'none') {
el.remove()
} else {
el.style.display = ''
}
return
}
// 统一处理为字符串并分割
const permissions = Array.isArray(value) ? value : value.split(',').map((p) => p.trim())
// 统一处理为字符串数组
const permissions = Array.isArray(value)
? value.map((p) => String(p).trim()).filter(Boolean)
: String(value)
.split(',')
.map((p) => p.trim())
.filter(Boolean)
// 如果过滤后为空(比如传入 ",," 这种情况),同空值处理
if (permissions.length === 0) {
if (mode === 'none') {
el.remove()
} else {
el.style.display = ''
}
return
}
// 白名单权限配置(可从后端获取或配置文件读取)
const WHITE_LIST = getStorage('btnAuth');
const WHITE_LIST: string[] = getStorage('btnAuth') || []
// 检查是否有任一权限在白名单中
const hasPermission = permissions.some((p) => WHITE_LIST.includes(p))
let hasPermission: boolean
// 根据 shouldContain 决定显示/隐藏
if (shouldContain) {
hasPermission ? (el.style.display = '') : el.remove()
} else {
hasPermission ? el.remove() : (el.style.display = '')
switch (mode) {
case 'some':
// 任一权限满足即显示
hasPermission = permissions.some((p) => WHITE_LIST.includes(p))
hasPermission ? (el.style.display = '') : el.remove()
break
case 'every':
// 全部权限满足才显示
hasPermission = permissions.every((p) => WHITE_LIST.includes(p))
hasPermission ? (el.style.display = '') : el.remove()
break
case 'none':
// 不包含任一权限时显示
hasPermission = permissions.some((p) => WHITE_LIST.includes(p))
hasPermission ? el.remove() : (el.style.display = '')
break
}
}
@ -62,5 +110,6 @@ function checkPermission(el: HTMLElement, value: string | string[], shouldContai
*/
export function setupAuthDirective(app: any) {
app.directive('auth', authDirective)
app.directive('auth-all', authAllDirective)
app.directive('auth-not', authNotDirective)
}

View File

@ -15,14 +15,14 @@
:highlight-current-row="false"
>
<!-- 操作列插槽 -->
<template #operation="{ row }">
<el-button type="primary" :disabled="row.status !== '1'" @click="handleAudit(row, 3)"
>审核通过</el-button
>
<el-button type="danger" :disabled="row.status !== '1'" @click="handleAudit(row, 4)"
>审核驳回</el-button
>
</template>
<!-- <template #operation="{ row }">-->
<!-- <el-button type="primary" :disabled="row.status !== '1'" @click="handleAudit(row, 3)"-->
<!-- >审核通过</el-button-->
<!-- >-->
<!-- <el-button type="danger" :disabled="row.status !== '1'" @click="handleAudit(row, 4)"-->
<!-- >审核驳回</el-button-->
<!-- >-->
<!-- </template>-->
</OrderRechangeTable>
<!-- 分页区域使用接口返回的 total / current_page / per_page -->

View File

@ -55,6 +55,6 @@ export const tableColumns = [
{ prop: 'statusText', label: '状态', align: 'center' as const },
{ prop: 'rechangeTime', label: '充值时间', align: 'center' as const },
{ prop: 'rechangeAccount', label: '充值账号', align: 'center' as const },
{ label: '操作', width: '240px', align: 'center' as const, slotName: 'operation' },
// { label: '操作', width: '240px', align: 'center' as const, slotName: 'operation' },
]

View File

@ -236,6 +236,7 @@ const getCustomerList = async () => {
const data: any = await getAgentListApi(params)
tableTree.value = data.data.map((item: any) => ({
...item,
accountName: item.username,
userId: item.id,
status: item.status === 1 ? '正常' : '禁用',

View File

@ -5,7 +5,7 @@ export const search = [
label: '账号名称',
type: 'input' as const,
placeholder: '请输入账号名称',
width: '150px',
width: '200px',
},
// 开始时间
{
@ -14,6 +14,7 @@ export const search = [
type: 'date' as const,
dateType: 'date' as const, // 时间范围选择器
valueFormat: 'YYYY-MM-DD',
width: '240px',
},
// 结束时间
{
@ -22,28 +23,30 @@ export const search = [
type: 'date' as const,
dateType: 'date' as const, // 时间范围选择器
valueFormat: 'YYYY-MM-DD',
width: '240px',
},
]
export const tableColumns = [
// 账号名称
{ label: '账号名称', prop: 'accountName'},
// 账号id
{ prop: 'userId', label: '账号id' },
// 状态
{ prop: 'status', label: '状态' },
// 账号角色
{ prop: 'role', label: '账号角色' },
// 账号数量
{ prop: 'accountQty', label: '账号数量' },
// 点差分配比例
{ prop: 'marginRatio', label: '点差分配比例', slotName: 'marginRatio' },
// 手续费分成比例
{ prop: 'commissionRatio', label: '手续费分成比例', slotName: 'commissionRatio' },
// 注册时间
{ prop: 'createTime', label: '注册时间', sortable: true },
// 操作
// { label: '操作', slotName: 'action' }
// 账号名称
{ label: '账号名称', prop: 'accountName' },
// 账号id
{ prop: 'userId', label: '账号id' },
// 状态
{ prop: 'status', label: '状态' },
// 账号角色
{ prop: 'role', label: '账号角色' },
// 账号数量
{ prop: 'accountQty', label: '账号数量' },
{ prop: 'wallet_capital.amount', label: '资金', align: 'center' as const },
// 点差分配比例
{ prop: 'marginRatio', label: '点差分配比例', slotName: 'marginRatio' },
// 手续费分成比例
{ prop: 'commissionRatio', label: '手续费分成比例', slotName: 'commissionRatio' },
// 注册时间
{ prop: 'createTime', label: '注册时间', sortable: true },
// 操作
// { label: '操作', slotName: 'action' }
]
// 点差表格配置

View File

@ -8,7 +8,7 @@
@reset="handleReset"
>
<template #button="{ form }">
<el-button type="primary" @click="handleAddMarket()" v-auth="'agent_createAgen'">
<el-button type="primary" @click="handleAddMarket()" v-auth="'agent_createAgent'">
添加
</el-button>
<el-button

View File

@ -50,12 +50,13 @@ export const clientSearch = [
//定义客户table列配置
export const clientTableColumns = [
{ prop: 'id', label: 'ID', align: 'center' as const },
{ prop: 'username', label: '用户名', align: 'center' as const },
{ prop: 'createdTime', label: '注册时间', align: 'center' as const },
{ prop: 'agentUsername', label: '所属代理', align: 'center' as const },
{ label: '状态', slotName: 'status', align: 'center' as const },
{ label: '操作', slotName: 'action', align: 'center' as const }
{ prop: 'id', label: 'ID', align: 'center' as const },
{ prop: 'username', label: '用户名', align: 'center' as const },
{ prop: 'createdTime', label: '注册时间', align: 'center' as const },
{ prop: 'agentUsername', label: '所属代理', align: 'center' as const },
{ prop: 'wallet_capital.amount', label: '资金', align: 'center' as const },
{ label: '状态', slotName: 'status', align: 'center' as const },
{ label: '操作', slotName: 'action', align: 'center' as const },
]
//定义添加客户form配置

View File

@ -107,7 +107,7 @@ const menuFormRef = ref<FormInstance>()
const menuFormRules = rules(menuForm.value, menuiconOptions)
// icon
const menuFormOptionsMap = ref({
parentName: [] as any[],
parent_id: [] as any[],
icon: menuiconOptions,
type: [
{ label: '菜单', value: 1 },
@ -142,7 +142,7 @@ const fetchMenuList = async () => {
label: item.name,
value: item.id ?? '',
}))
menuFormOptionsMap.value.parentName = [{ label: '一级菜单', value: '0' }, ...parentNameOptions]
menuFormOptionsMap.value.parent_id = [{ label: '一级菜单', value: '0' }, ...parentNameOptions]
} catch (err) {
console.error('获取菜单列表失败', err)
}
@ -187,7 +187,7 @@ const handleSubmit = async () => {
if (dialogType.value === 'add') {
const params = {
name: menuForm.value.name, //
parent_id: menuForm.value.parentName, // id
parent_id: menuForm.value.parent_id, // id
path: menuForm.value.path, //
api: menuForm.value.api, //
icon: menuForm.value.icon, //
@ -213,6 +213,7 @@ const handleCancel = () => {
menuForm.value = {
name: '', //
parentName: '', //
parent_id: '', //
path: '', //
api: '', //
icon: '', //
@ -227,6 +228,7 @@ const handleClose = () => {
menuForm.value = {
name: '', //
parentName: '', //
parent_id: '', //
path: '', //
api: '', //
icon: '', //

View File

@ -37,12 +37,17 @@ export const menuTableColumns = [
//定义menuForm配置
export const menuFormItemOptions = [
{ prop: 'name', label: '菜单名称', type: 'input' as const,placeholder: '请输入菜单名称' },
{ prop: 'parentName', label: '父级菜单', type: 'select' as const, placeholder: '请选择父级菜单' },
{ prop: 'path', label: '前端路由', type: 'input' as const, placeholder: '请输入前端路由路径' },
{ prop: 'api', label: '后端路由', type: 'input' as const, placeholder: '请输入后端路由' },
{ prop: 'icon', label: '图标名称', type: 'select' as const, placeholder: '请选择图标名称' },
{ prop: 'sort', label: '排序', type: 'number' as const, placeholder: '请输入排序,数字越小越靠前' },
{ prop: 'type', label: '菜单类型', type: 'radio' as const, placeholder: '请选择菜单类型' },
{ prop: 'status', label: '状态', type: 'radio' as const, placeholder: '请选择状态' },
{ prop: 'name', label: '菜单名称', type: 'input' as const, placeholder: '请输入菜单名称' },
{ prop: 'parent_id', label: '父级菜单', type: 'select' as const, placeholder: '请选择父级菜单' },
{ prop: 'path', label: '前端路由', type: 'input' as const, placeholder: '请输入前端路由路径' },
{ prop: 'api', label: '后端路由', type: 'input' as const, placeholder: '请输入后端路由' },
{ prop: 'icon', label: '图标名称', type: 'select' as const, placeholder: '请选择图标名称' },
{
prop: 'sort',
label: '排序',
type: 'number' as const,
placeholder: '请输入排序,数字越小越靠前',
},
{ prop: 'type', label: '菜单类型', type: 'radio' as const, placeholder: '请选择菜单类型' },
{ prop: 'status', label: '状态', type: 'radio' as const, placeholder: '请选择状态' },
]

View File

@ -7,7 +7,7 @@ export const search = [
label: '公告名称',
type: 'input' as const,
placeholder: '请输入公告名称',
width: '120px',
width: '220px',
},
{
prop: 'startDateTime',
@ -15,7 +15,7 @@ export const search = [
type: 'date' as const,
dateType: 'date' as const, // 时间范围选择器
valueFormat: 'YYYY-MM-DD',
width: '180px',
width: '220px',
},
{
prop: 'endDateTime',
@ -23,7 +23,7 @@ export const search = [
type: 'date' as const,
dateType: 'date' as const, // 时间范围选择器
valueFormat: 'YYYY-MM-DD',
width: '180px',
width: '220px',
},
{
prop: 'status',
@ -34,6 +34,7 @@ export const search = [
{ label: '草稿', value: 1 },
{ label: '已发布', value: 2 },
],
width: '220px',
},
]

View File

@ -12,7 +12,7 @@
type="primary"
:disabled="!selectedRow"
@click="handleEdit()"
v-auth="'contracVarietediariety'"
v-auth="'contractVariety_editVariety'"
>
编辑
</el-button>
@ -57,11 +57,13 @@
>
<template #upload="{ formData }">
<el-upload
ref="uploadRef"
v-model:file-list="fileList"
class="upload-demo"
:http-request="handleUpload"
:before-upload="beforeUpload"
:limit="1"
:on-exceed="handleExceed"
>
<el-button type="primary">点击上传</el-button>
<template #tip>
@ -287,6 +289,19 @@ const handleReset = () => {
const handleEdit = () => {
getTradeGroupList()
const row = selectedRow.value
if (row.image) {
fileList.value = [
{
name: row.image.split('/').pop() || 'existing.jpg',
url: row.image,
status: 'success',
},
]
} else {
fileList.value = []
}
varietyForm.value = {
...row,
id: row.id,
@ -367,6 +382,21 @@ const handleUpload = async (options: any) => {
}
}
const uploadRef = ref()
const handleExceed = (files: File[]) => {
//
if (uploadRef.value) {
uploadRef.value.clearFiles()
}
//
const file = files[0]
if (file) {
uploadRef.value?.handleStart(file) //
// http-request
handleUpload({ file, onSuccess: () => {}, onError: () => {} })
}
}
// + 20KB = 20 * 1024
const beforeUpload = (file: File) => {
//
@ -446,6 +476,7 @@ const handleCancel = () => {
dialogVisible.value = false
}
const handleClose = () => {
fileList.value = []
dialogVisible.value = false
}

View File

@ -84,7 +84,7 @@ const handleInput = (index: number, val: string) => {
const handleFocus = (index: number) => {
//
inputList[index] = '';
// inputList[index] = '';
}
//