backup: kefu state before examiner-add feature on ExaminerManage
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
269
src/views/admin/ClubConfig.vue
Normal file
269
src/views/admin/ClubConfig.vue
Normal 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>
|
||||
534
src/views/admin/DataScope.vue
Normal file
534
src/views/admin/DataScope.vue
Normal 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>
|
||||
|
||||
|
||||
312
src/views/admin/OperationLog.vue
Normal file
312
src/views/admin/OperationLog.vue
Normal 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>
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user