backup: kefu state before examiner-add feature on ExaminerManage

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-06-28 00:31:26 +08:00
parent a7f7bd0c39
commit b161077400
53 changed files with 6254 additions and 567 deletions

View File

@@ -0,0 +1,269 @@
<template>
<div class="club-config-page">
<el-alert
type="warning"
:closable="false"
show-icon
title="俱乐部主数据与小程序/支付/GoEasy 密钥"
description="仅超级管理员可编辑。修改后影响该俱乐部小程序登录、支付回调、IM 等,请谨慎操作。"
class="tip"
/>
<div class="toolbar">
<el-select v-model="currentClubId" placeholder="选择俱乐部" @change="loadClub" style="width: 200px">
<el-option v-for="c in clubList" :key="c.club_id" :label="c.name" :value="c.club_id" />
</el-select>
<el-button type="success" @click="openCreateDialog">新建俱乐部</el-button>
<el-button type="primary" :loading="saving" @click="saveClub">保存配置</el-button>
<el-button :icon="Refresh" circle @click="loadClub" :loading="loading" />
</div>
<el-dialog v-model="createVisible" title="新建俱乐部 / 小程序" width="480px">
<el-form label-width="120px">
<el-form-item label="俱乐部 ID" required>
<el-input v-model="createForm.new_club_id" placeholder="如 xy、al与小程序 club-config 一致)" />
</el-form-item>
<el-form-item label="展示名称" required>
<el-input v-model="createForm.name" placeholder="如:逍遥梦俱乐部" />
</el-form-item>
<el-form-item label="小程序 AppID">
<el-input v-model="createForm.wx_appid" placeholder="新建小程序的 AppID" />
</el-form-item>
<el-form-item label="复制模板">
<el-select v-model="createForm.from_club" style="width: 100%">
<el-option v-for="c in clubList" :key="c.club_id" :label="c.name" :value="c.club_id" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="createVisible = false">取消</el-button>
<el-button type="primary" :loading="creating" @click="createClub">创建</el-button>
</template>
</el-dialog>
<el-form v-loading="loading" :model="form" label-width="140px" class="config-form">
<el-divider content-position="left">小程序微信</el-divider>
<el-form-item label="wx_appid">
<el-input v-model="form.wx_appid" placeholder="小程序 AppID" />
</el-form-item>
<el-form-item label="wx_secret">
<el-input v-model="form.wx_secret" type="password" show-password placeholder="小程序 Secret" />
</el-form-item>
<el-divider content-position="left">微信支付</el-divider>
<el-form-item label="商户号 mch_id">
<el-input v-model="form.mch_id" />
</el-form-item>
<el-form-item label="pay_app_id">
<el-input v-model="form.pay_app_id" />
</el-form-item>
<el-form-item label="api_v3_key">
<el-input v-model="form.api_v3_key" type="password" show-password />
</el-form-item>
<el-form-item label="cert_serial_no">
<el-input v-model="form.cert_serial_no" />
</el-form-item>
<el-form-item label="private_key_path">
<el-input v-model="form.private_key_path" placeholder="服务器上私钥文件路径" />
</el-form-item>
<el-form-item label="platform_cert_dir">
<el-input v-model="form.platform_cert_dir" placeholder="平台证书目录" />
</el-form-item>
<el-divider content-position="left">服务号</el-divider>
<el-form-item label="official_appid">
<el-input v-model="form.official_appid" />
</el-form-item>
<el-form-item label="official_secret">
<el-input v-model="form.official_secret" type="password" show-password />
</el-form-item>
<el-form-item label="official_token">
<el-input v-model="form.official_token" />
</el-form-item>
<el-form-item label="encoding_aes_key">
<el-input v-model="form.encoding_aes_key" />
</el-form-item>
<el-divider content-position="left">GoEasy / OSS / 域名</el-divider>
<el-form-item label="goeasy_appkey">
<el-input v-model="form.goeasy_appkey" />
</el-form-item>
<el-form-item label="goeasy_secret">
<el-input v-model="form.goeasy_secret" type="password" show-password />
</el-form-item>
<el-form-item label="oss_prefix">
<el-input v-model="form.oss_prefix" placeholder="同一 COS 桶内目录前缀,如 xq/" />
</el-form-item>
<el-form-item label="h5_domain">
<el-input v-model="form.h5_domain" placeholder="商家 H5 域名" />
</el-form-item>
<el-form-item label="template_id">
<el-input v-model="form.template_id" />
</el-form-item>
<el-form-item label="展示名称">
<el-input v-model="form.name" />
</el-form-item>
</el-form>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { Refresh } from '@element-plus/icons-vue'
import request from '@/utils/request'
import { JITUAN_API } from '@/utils/club-context'
const loading = ref(false)
const saving = ref(false)
const creating = ref(false)
const createVisible = ref(false)
const clubList = ref([])
const currentClubId = ref('xq')
const createForm = reactive({
new_club_id: '',
name: '',
wx_appid: '',
from_club: 'xq',
})
const form = reactive({
name: '',
wx_appid: '',
wx_secret: '',
mch_id: '',
pay_app_id: '',
api_v3_key: '',
cert_serial_no: '',
private_key_path: '',
platform_cert_dir: '',
official_appid: '',
official_secret: '',
official_token: '',
encoding_aes_key: '',
goeasy_appkey: '',
goeasy_secret: '',
oss_prefix: '',
h5_domain: '',
template_id: '',
template_max_per_minute: 950,
sort_order: 0,
status: 1,
})
const phone = localStorage.getItem('username')
const loadClubList = async () => {
const res = await request.post(JITUAN_API.clubManage, { phone, action: 'list' })
if (res.code === 0) {
clubList.value = res.data || []
if (!currentClubId.value && clubList.value.length) {
currentClubId.value = clubList.value[0].club_id
}
}
}
const loadClub = async () => {
if (!currentClubId.value) return
loading.value = true
try {
const res = await request.post(JITUAN_API.clubManage, {
phone,
action: 'get',
club_id: currentClubId.value,
})
if (res.code === 0 && res.data) {
Object.assign(form, res.data)
} else {
ElMessage.error(res.msg || '加载失败')
}
} catch {
ElMessage.error('加载俱乐部配置失败,请确认已部署最新后端且账号为超级管理员')
} finally {
loading.value = false
}
}
const saveClub = async () => {
saving.value = true
try {
const res = await request.post(JITUAN_API.clubManage, {
phone,
action: 'update',
club_id: currentClubId.value,
...form,
})
if (res.code === 0) {
ElMessage.success('保存成功')
await loadClubList()
} else {
ElMessage.error(res.msg || '保存失败')
}
} catch {
ElMessage.error('保存失败')
} finally {
saving.value = false
}
}
const openCreateDialog = () => {
createForm.new_club_id = ''
createForm.name = ''
createForm.wx_appid = ''
createForm.from_club = currentClubId.value || 'xq'
createVisible.value = true
}
const createClub = async () => {
if (!createForm.new_club_id || !createForm.name) {
ElMessage.warning('请填写俱乐部 ID 和名称')
return
}
creating.value = true
try {
const res = await request.post(JITUAN_API.clubManage, {
phone,
action: 'create',
new_club_id: createForm.new_club_id.trim(),
name: createForm.name.trim(),
wx_appid: createForm.wx_appid.trim(),
from_club: createForm.from_club,
})
if (res.code === 0) {
ElMessage.success('俱乐部创建成功,请继续填写密钥并保存')
createVisible.value = false
await loadClubList()
currentClubId.value = createForm.new_club_id.trim()
await loadClub()
} else {
ElMessage.error(res.msg || '创建失败')
}
} catch {
ElMessage.error('创建失败,请确认账号有 000001 权限且后端已部署')
} finally {
creating.value = false
}
}
onMounted(async () => {
await loadClubList()
await loadClub()
})
</script>
<style scoped>
.club-config-page {
padding: 8px 4px;
}
.tip {
margin-bottom: 16px;
}
.toolbar {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
}
.config-form {
max-width: 720px;
}
</style>

View File

@@ -0,0 +1,534 @@
<template>
<div class="data-scope-page">
<el-alert
type="warning"
:closable="false"
show-icon
class="scope-tip"
title="操作权限在「角色管理」里配"
description="查看会员=3300a、财务=caiwu、订单=002ab 等:角色管理 → 角色详情 → 添加权限。给账号绑角色:管理员用户。"
/>
<el-alert
type="info"
:closable="false"
show-icon
class="scope-tip"
title="集团高管 / 查看全部订单与用户"
description="功能权限:角色管理里给「订单 002ab」「用户列表」等权限。数据范围本页添加任职范围选「全部俱乐部 ALL_CLUBS」登录后顶栏切「集团汇总」即可看全部订单/财务/用户(统计按各俱乐部汇总,不串单俱乐部数据)。"
/>
<div class="action-bar">
<el-tag type="info" size="large">任职记录 {{ list.length }} </el-tag>
<div class="action-right">
<el-button v-if="canManage" type="primary" @click="openAssignDialog">添加任职</el-button>
<el-button :icon="Refresh" circle @click="fetchList" :loading="loading" />
</div>
</div>
<el-table :data="list" v-loading="loading" border stripe>
<el-table-column prop="id" label="ID" width="70" />
<el-table-column prop="phone" label="后台账号" width="140" />
<el-table-column prop="yonghuid" label="用户ID" width="100" />
<el-table-column prop="club_label" label="数据范围" min-width="160" />
<el-table-column prop="role_name" label="任职角色" width="160" />
<el-table-column prop="data_scope" label="范围类型" width="120">
<template #default="{ row }">
{{ row.data_scope === 'ALL_CLUBS' ? '集团汇总' : '单俱乐部' }}
</template>
</el-table-column>
<el-table-column label="主任职" width="90" align="center">
<template #default="{ row }">
<el-tag v-if="row.is_primary" type="success" size="small"></el-tag>
<span v-else></span>
</template>
</el-table-column>
<el-table-column v-if="canManage" label="操作" width="100" align="center">
<template #default="{ row }">
<el-button type="danger" link @click="removeAssignment(row)">停用</el-button>
</template>
</el-table-column>
</el-table>
<el-card v-if="currentContext" class="context-card" shadow="never">
<template #header>当前登录上下文</template>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="任职角色">{{ currentContext.role_name }}</el-descriptions-item>
<el-descriptions-item label="数据范围">
{{ currentContext.scope === 'ALL_CLUBS' ? '集团汇总' : `子公司 ${currentContext.club_id}` }}
</el-descriptions-item>
<el-descriptions-item label="可切换俱乐部">
{{ currentContext.can_switch_club ? '是' : '否' }}
</el-descriptions-item>
<el-descriptions-item label="集团管理员">
{{ currentContext.is_group_admin ? '是' : '否' }}
</el-descriptions-item>
</el-descriptions>
</el-card>
<el-dialog v-model="assignVisible" title="添加数据范围任职" width="480px">
<el-form label-width="120px">
<el-form-item label="后台手机号" required>
<el-input v-model="assignForm.target_phone" placeholder="客服登录手机号" />
</el-form-item>
<el-form-item label="数据范围">
<el-select v-model="assignForm.club_id" placeholder="留空=集团全部" clearable style="width: 100%">
<el-option label="集团(全部子公司)" value="" />
<el-option
v-for="c in clubOptions"
:key="c.club_id"
:label="c.name"
:value="c.club_id"
/>
</el-select>
</el-form-item>
<el-form-item label="任职角色">
<el-select
v-model="assignForm.role_code"
filterable
allow-create
default-first-option
placeholder="可选预设或自定义输入(仅标签,不控制菜单)"
style="width: 100%"
>
<el-option label="集团总负责人" value="GROUP_OWNER" />
<el-option label="集团超管" value="GROUP_SUPER_ADMIN" />
<el-option label="集团财务" value="GROUP_FINANCE" />
<el-option label="集团售后" value="GROUP_AFTER_SALES" />
<el-option label="俱乐部总负责人" value="CLUB_OWNER" />
<el-option label="俱乐部管理员" value="CLUB_ADMIN" />
<el-option label="俱乐部财务" value="CLUB_FINANCE" />
<el-option label="俱乐部售后" value="CLUB_AFTER_SALES" />
<el-option label="俱乐部运营" value="CLUB_OPERATOR" />
</el-select>
<div class="role-hint">任职角色只是备注标签<strong>不能</strong>替代角色管理里的功能权限</div>
</el-form-item>
<el-form-item label="主任职">
<el-switch v-model="assignForm.is_primary" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="assignVisible = false">取消</el-button>
<el-button type="primary" :loading="assigning" @click="submitAssign">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Refresh } from '@element-plus/icons-vue'
import request from '@/utils/request'
import { JITUAN_API } from '@/utils/club-context'
const loading = ref(false)
const assigning = ref(false)
const assignVisible = ref(false)
const list = ref([])
const permNote = ref('')
const currentContext = ref(null)
const canManage = ref(false)
const clubOptions = ref([])
const loadClubOptionsForAssign = async () => {
try {
const phone = localStorage.getItem('username')
const res = await request.post(JITUAN_API.clubManage, { phone, action: 'list' })
if (res.code === 0 && Array.isArray(res.data?.list)) {
clubOptions.value = res.data.list
.filter((c) => c.status !== 0)
.map((c) => ({ club_id: c.club_id, name: c.name }))
return
}
} catch {
// ignore
}
if (currentContext.value?.clubs?.length) {
clubOptions.value = currentContext.value.clubs
}
}
const assignForm = reactive({
target_phone: '',
club_id: '',
role_code: 'CLUB_ADMIN',
is_primary: false,
})
const fetchList = async () => {
loading.value = true
try {
const phone = localStorage.getItem('username')
const res = await request.post(JITUAN_API.adminAssignments, { phone })
if (res.code === 0) {
list.value = res.data?.list || []
permNote.value = res.data?.perm_note || ''
currentContext.value = res.data?.current_context || null
canManage.value = res.data?.can_manage || false
clubOptions.value = currentContext.value?.clubs || []
if (canManage.value) {
await loadClubOptionsForAssign()
}
return
}
ElMessage.error(res.msg || '加载失败')
} catch (err) {
try {
const ctxRes = await request.get(JITUAN_API.meContext)
if (ctxRes.code === 0 && ctxRes.data) {
currentContext.value = ctxRes.data
clubOptions.value = ctxRes.data.clubs || []
permNote.value = '任职列表接口暂不可用;请部署最新后端并执行 migrate jituan。'
ElMessage.warning('任职列表加载失败,已显示当前登录上下文')
return
}
} catch {
// ignore
}
console.error(err)
ElMessage.error('加载失败,请确认已部署 /jituan/houtai/admin-assignments')
} finally {
loading.value = false
}
}
const openAssignDialog = async () => {
assignForm.target_phone = ''
assignForm.club_id = ''
assignForm.role_code = 'CLUB_ADMIN'
assignForm.is_primary = false
if (canManage.value) {
await loadClubOptionsForAssign()
}
assignVisible.value = true
}
const submitAssign = async () => {
if (!assignForm.target_phone) {
ElMessage.warning('请填写手机号')
return
}
assigning.value = true
try {
const phone = localStorage.getItem('username')
const res = await request.post(JITUAN_API.adminAssignments, {
phone,
action: 'create',
target_phone: assignForm.target_phone.trim(),
club_id: assignForm.club_id || null,
role_code: assignForm.role_code,
is_primary: assignForm.is_primary,
data_scope: assignForm.club_id ? 'SINGLE_CLUB' : 'ALL_CLUBS',
})
if (res.code === 0) {
ElMessage.success('任职已添加')
assignVisible.value = false
await fetchList()
} else {
ElMessage.error(res.msg || '添加失败')
}
} catch {
ElMessage.error('添加失败')
} finally {
assigning.value = false
}
}
const removeAssignment = (row) => {
ElMessageBox.confirm(`停用 ${row.phone} 的任职?`, '提示', { type: 'warning' })
.then(async () => {
const phone = localStorage.getItem('username')
const res = await request.post(JITUAN_API.adminAssignments, {
phone,
action: 'delete',
id: row.id,
})
if (res.code === 0) {
ElMessage.success('已停用')
list.value = list.value.filter((item) => item.id !== row.id)
await fetchList()
} else {
ElMessage.error(res.msg || '操作失败')
}
})
.catch(() => {})
}
onMounted(fetchList)
</script>
<style scoped>
.data-scope-page {
padding: 0 4px;
}
.scope-tip {
margin-bottom: 16px;
}
.action-bar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.action-right {
display: flex;
gap: 8px;
align-items: center;
}
.context-card {
margin-top: 20px;
}
.role-hint {
font-size: 12px;
color: #8ab3cf;
margin-top: 6px;
line-height: 1.4;
}
</style>

View File

@@ -0,0 +1,312 @@
<template>
<div class="operation-log">
<div v-if="!hasPermission" class="no-permission">
<div class="no-permission-icon">🔒</div>
<div class="no-permission-text">您无权访问操作日志</div>
<div class="no-permission-desc">需要 000001 czrz666 权限</div>
</div>
<div v-else>
<div class="page-header">
<h2>操作日志</h2>
<span class="subtitle">后台对用户/商家/管事/组长等数据的修改记录</span>
</div>
<div class="log-type-tabs">
<span
class="log-type-tab"
:class="{ active: logType === 'xiugai' }"
@click="switchLogType('xiugai')"
>修改记录</span>
<span
class="log-type-tab"
:class="{ active: logType === 'audit' }"
@click="switchLogType('audit')"
>审计日志</span>
</div>
<div class="filter-bar" v-if="logType === 'xiugai'">
<el-input v-model="filters.yonghuid" placeholder="被修改用户ID" clearable class="filter-item" />
<el-input v-model="filters.xiugaiid" placeholder="操作人账号" clearable class="filter-item" />
<el-select v-model="filters.leixing" placeholder="用户类型" clearable class="filter-item">
<el-option label="打手" :value="2" />
<el-option label="管事" :value="3" />
<el-option label="商家" :value="4" />
<el-option label="组长" :value="5" />
</el-select>
<el-input v-model="filters.keyword" placeholder="搜索操作说明" clearable class="filter-item wide" />
<el-button type="primary" @click="handleSearch" :loading="loading">查询</el-button>
<el-button @click="handleReset">重置</el-button>
</div>
<div class="filter-bar" v-else>
<el-input v-model="auditFilters.target_yonghuid" placeholder="目标用户ID" clearable class="filter-item" />
<el-input v-model="auditFilters.operator_yonghuid" placeholder="操作人ID" clearable class="filter-item" />
<el-input v-model="auditFilters.keyword" placeholder="搜索备注" clearable class="filter-item wide" />
<el-button type="primary" @click="handleSearch" :loading="loading">查询</el-button>
<el-button @click="handleReset">重置</el-button>
</div>
<el-table v-if="logType === 'xiugai'" :data="list" v-loading="loading" stripe border class="log-table">
<el-table-column prop="create_time" label="时间" width="170" />
<el-table-column prop="leixing_label" label="类型" width="80" />
<el-table-column prop="yonghuid" label="被修改用户" width="100" />
<el-table-column prop="xiugaiid" label="操作人" width="130" />
<el-table-column prop="qitashuoming" label="操作说明" min-width="360">
<template #default="{ row }">
<div class="desc-cell">{{ row.qitashuoming || '--' }}</div>
</template>
</el-table-column>
<el-table-column label="金额/积分变动" width="200">
<template #default="{ row }">
<div class="amount-cell">
<div v-if="row.xiugaitijiaoq !== null && row.xiugaitijiao !== null">
余额: {{ row.xiugaitijiaoq }} {{ row.xiugaitijiao }}
</div>
<div v-if="row.shangjiayueq !== null && row.shangjiayue !== null && row.shangjiayueq !== '0.00'">
商家余额: {{ row.shangjiayueq }} {{ row.shangjiayue }}
</div>
<div v-if="row.guanshiyueq !== null && row.guanshiyue !== null && row.guanshiyueq !== '0.00'">
管事余额: {{ row.guanshiyueq }} {{ row.guanshiyue }}
</div>
<div v-if="row.yjifen || row.jifen">
积分: {{ row.yjifen }} {{ row.jifen }}
</div>
<div v-if="row.yyajin !== null && row.yajin !== null && row.yyajin !== '0.00'">
押金: {{ row.yyajin }} {{ row.yajin }}
</div>
</div>
</template>
</el-table-column>
</el-table>
<el-table v-else :data="auditList" v-loading="loading" stripe border class="log-table">
<el-table-column prop="CreateTime" label="时间" width="170" />
<el-table-column prop="club_id" label="俱乐部" width="90" />
<el-table-column prop="operator_yonghuid" label="操作人" width="100" />
<el-table-column prop="target_yonghuid" label="目标用户" width="100" />
<el-table-column prop="action" label="动作" width="100" />
<el-table-column prop="remark" label="备注" min-width="360">
<template #default="{ row }">
<div class="desc-cell">{{ row.remark || '--' }}</div>
</template>
</el-table-column>
<el-table-column prop="request_ip" label="IP" width="130" />
</el-table>
<div class="pagination-wrap">
<el-pagination
v-model:current-page="page"
v-model:page-size="pageSize"
:total="total"
:page-sizes="[20, 50, 100]"
layout="total, sizes, prev, pager, next"
@current-change="fetchList"
@size-change="handleSizeChange"
/>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted, inject } from 'vue'
import { ElMessage } from 'element-plus'
import request from '@/utils/request'
import { JITUAN_API } from '@/utils/club-context'
const username = ref(localStorage.getItem('username') || '')
const kefuPermissions = inject('kefuPermissions', ref([]))
const logType = ref('xiugai')
const hasPermission = computed(() => {
const perms = kefuPermissions.value || []
return perms.includes('000001') || perms.includes('czrz666')
})
const loading = ref(false)
const list = ref([])
const auditList = ref([])
const total = ref(0)
const page = ref(1)
const pageSize = ref(20)
const filters = reactive({
yonghuid: '',
xiugaiid: '',
leixing: null,
keyword: ''
})
const auditFilters = reactive({
target_yonghuid: '',
operator_yonghuid: '',
keyword: ''
})
const fetchXiugaiList = async () => {
const res = await request.post(JITUAN_API.operationLog, {
username: username.value,
page: page.value,
page_size: pageSize.value,
yonghuid: filters.yonghuid.trim(),
xiugaiid: filters.xiugaiid.trim(),
leixing: filters.leixing,
keyword: filters.keyword.trim()
})
if (res.code === 0) {
list.value = res.data?.list || []
total.value = res.data?.total || 0
} else {
ElMessage.error(res.msg || '获取日志失败')
}
}
const fetchAuditList = async () => {
const res = await request.post(JITUAN_API.auditLog, {
username: username.value,
page: page.value,
page_size: pageSize.value,
target_yonghuid: auditFilters.target_yonghuid.trim(),
operator_yonghuid: auditFilters.operator_yonghuid.trim(),
keyword: auditFilters.keyword.trim()
})
if (res.code === 0) {
auditList.value = res.data?.list || []
total.value = res.data?.total || 0
} else {
ElMessage.error(res.msg || '获取审计日志失败')
}
}
const fetchList = async () => {
if (!hasPermission.value) return
loading.value = true
try {
if (logType.value === 'audit') {
await fetchAuditList()
} else {
await fetchXiugaiList()
}
} catch (e) {
console.error(e)
ElMessage.error('获取日志失败')
} finally {
loading.value = false
}
}
const switchLogType = (type) => {
if (logType.value === type) return
logType.value = type
page.value = 1
fetchList()
}
const handleSearch = () => {
page.value = 1
fetchList()
}
const handleReset = () => {
if (logType.value === 'audit') {
auditFilters.target_yonghuid = ''
auditFilters.operator_yonghuid = ''
auditFilters.keyword = ''
} else {
filters.yonghuid = ''
filters.xiugaiid = ''
filters.leixing = null
filters.keyword = ''
}
page.value = 1
fetchList()
}
const handleSizeChange = () => {
page.value = 1
fetchList()
}
onMounted(() => {
if (hasPermission.value) fetchList()
})
</script>
<style scoped>
.operation-log {
padding: 20px;
}
.page-header {
margin-bottom: 20px;
}
.log-type-tabs {
display: flex;
gap: 12px;
margin-bottom: 16px;
}
.log-type-tab {
padding: 6px 16px;
border-radius: 8px;
cursor: pointer;
background: #f5f5f5;
color: #666;
}
.log-type-tab.active {
background: #409eff;
color: #fff;
}
.page-header h2 {
margin: 0 0 4px;
font-size: 20px;
}
.subtitle {
color: #909399;
font-size: 13px;
}
.filter-bar {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 16px;
align-items: center;
}
.filter-item {
width: 160px;
}
.filter-item.wide {
width: 220px;
}
.desc-cell {
white-space: pre-wrap;
word-break: break-all;
line-height: 1.5;
color: #303133;
font-weight: 500;
}
.amount-cell {
font-size: 12px;
color: #606266;
line-height: 1.6;
}
.pagination-wrap {
margin-top: 16px;
display: flex;
justify-content: flex-end;
}
.no-permission {
text-align: center;
padding: 80px 20px;
}
.no-permission-icon {
font-size: 48px;
margin-bottom: 12px;
}
.no-permission-text {
font-size: 18px;
font-weight: 600;
}
.no-permission-desc {
color: #909399;
margin-top: 8px;
}
</style>

View File

@@ -8,6 +8,14 @@
</div>
<div v-else class="manager-container">
<el-alert
type="warning"
:closable="false"
show-icon
class="scope-hint"
title="角色按当前顶栏俱乐部隔离"
description="在「星之界 xzj」视图下添加的角色仅 xzj 可见权限码caiwu、002ab 等)全局共用。集团高管:顶栏选「集团汇总」+ 数据范围配 ALL_CLUBS并给角色订单/用户/财务权限。"
/>
<!-- 顶部操作栏 -->
<div class="action-bar">
<div class="stats-tip">
@@ -23,7 +31,8 @@
<div v-for="role in roleList" :key="role.role_code" class="role-card">
<div class="card-header">
<div class="role-name">{{ role.role_name }}</div>
<!-- 不展示角色编码 -->
<el-tag v-if="role.club_id" size="small" type="info">{{ role.club_id }}</el-tag>
<el-tag v-else size="small">集团全局</el-tag>
</div>
<div class="card-body">
<div class="desc">{{ role.description || '暂无描述' }}</div>
@@ -446,6 +455,9 @@ onMounted(() => {
color: rgba(255,255,255,0.5);
}
.scope-hint {
margin-bottom: 16px;
}
.manager-container {
max-width: 1400px;
margin: 0 auto;