diff --git a/src/router/index.js b/src/router/index.js index 0448a69..9333059 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -3,6 +3,7 @@ import Layout from '@/views/Layout.vue' import request from '@/utils/request' import { ensureMenuAccess, + syncClubContextFromServer, getMenuAccess, getDefaultLandingPath, getEffectiveVisiblePaths, @@ -362,6 +363,10 @@ router.beforeEach(async (to, from, next) => { } try { + await syncClubContextFromServer(async () => { + const res = await request.get(JITUAN_API.meContext, { skipErrorToast: true }) + return res.code === 0 ? res.data : null + }) await ensureMenuAccess(async () => { const res = await request.get(JITUAN_API.menuAccess) return res.code === 0 ? res.data : null diff --git a/src/utils/club-context.js b/src/utils/club-context.js index b83f01c..2c70752 100644 --- a/src/utils/club-context.js +++ b/src/utils/club-context.js @@ -3,7 +3,7 @@ const CLUB_SCOPE_KEY = 'admin_club_scope'; const CLUB_CONTEXT_KEY = 'admin_club_context'; const MENU_ACCESS_KEY = 'kefu_menu_access'; const MENU_SESSION_KEY = 'kefu_menu_session_v'; -const MENU_SESSION_VERSION = '19'; +const MENU_SESSION_VERSION = '21'; const DEFAULT_CLUB_ID = 'xq'; let menuAccessInflight = null; @@ -59,6 +59,10 @@ export function mergeServerClubContext(serverCtx) { } } + if (prevClubId && prevClubId !== clubId) { + clearMenuAccess(); + } + setAdminClubContext(merged); return merged; } @@ -253,6 +257,21 @@ export function canSwitchClubByMenu() { return !!access?.can_switch_club; } +export async function syncClubContextFromServer(fetcher) { + const prevClubId = getAdminClubId(); + try { + const data = await fetcher(); + if (data) { + mergeServerClubContext(data); + if (getAdminClubId() !== prevClubId) { + clearMenuAccess(); + } + } + } catch (e) { + console.error('syncClubContextFromServer failed:', e); + } +} + export async function ensureMenuAccess(fetcher, { force = false } = {}) { if (!localStorage.getItem('token')) return getMenuAccess(); if (!force && isMenuSessionFresh() && getMenuAccess()) { diff --git a/src/utils/penalty-status.js b/src/utils/penalty-status.js new file mode 100644 index 0000000..1f77ccd --- /dev/null +++ b/src/utils/penalty-status.js @@ -0,0 +1,30 @@ +/** 金额罚单状态(与 Django utils/penalty_status.py 一致) */ +export const PENALTY_STATUS_MAP = { + 1: { text: '待缴纳', type: 'danger' }, + 2: { text: '已缴纳', type: 'success' }, + 3: { text: '申诉中', type: 'warning' }, + 4: { text: '已驳回', type: 'info' }, + 5: { text: '平台审核中', type: 'warning' }, +} + +export const penaltyStatusText = (code) => PENALTY_STATUS_MAP[code]?.text || '未知' +export const penaltyStatusType = (code) => PENALTY_STATUS_MAP[code]?.type || '' + +/** 列表行展示状态(优先用后端 zhuangtai_text) */ +export const rowPenaltyStatusText = (row) => { + if (row?.zhuangtai_text) return row.zhuangtai_text + if (row?.platform_audit_pending) return '平台审核中' + return penaltyStatusText(Number(row?.zhuangtai)) +} + +export const rowPenaltyStatusType = (row) => { + if (row?.platform_audit_pending) return 'warning' + return penaltyStatusType(Number(row?.zhuangtai)) +} + +/** 是否可平台审核(仅 zhuangtai=5) */ +export const isPlatformAuditPending = (row) => { + if (!row) return false + if (row.platform_audit_pending === true) return true + return Number(row.zhuangtai) === 5 +} diff --git a/src/views/admin/AdminUser.vue b/src/views/admin/AdminUser.vue index 5f09616..c0ea129 100644 --- a/src/views/admin/AdminUser.vue +++ b/src/views/admin/AdminUser.vue @@ -43,7 +43,7 @@ 搜索 重置 - + 添加用户 @@ -399,6 +399,10 @@ const roleClubLabel = (role) => { return `[${cname}] ${role.role_name || role.role_code || ''}` } +const canAddUser = computed(() => { + return canManageClubAssignments.value || manageableClubIds.value.length > 0 +}) + const canManageRolesForClub = (clubId) => { const cid = clubId || '' if (canManageClubAssignments.value) return true @@ -411,7 +415,7 @@ const visibleRolesForRow = (row) => { return roles.filter((r) => manageableClubIds.value.includes(r.club_id || '')) } -// 权限状态 +// 权限状态:集团高管 / 俱乐部超管 / 000001 均可访问 const hasPermission = ref(true) const canManageClubAssignments = ref(false) const manageableClubIds = ref([]) @@ -605,6 +609,8 @@ const fetchUserList = async () => { total.value = res.data.total || 0 canManageClubAssignments.value = !!res.data.can_manage_club_assignments manageableClubIds.value = res.data.manageable_club_ids || [] + } else if (res.code === 403) { + hasPermission.value = false } else { ElMessage.error(res.msg || '获取用户列表失败') } @@ -853,7 +859,7 @@ const confirmAddUser = async () => { } } -// 初始化 +// 初始化:先拉用户列表(含 manageable_club_ids),再拉角色列表 onMounted(async () => { if (!username) { ElMessage.error('未获取到用户信息') @@ -861,7 +867,9 @@ onMounted(async () => { } await loadClubOptions() await fetchUserList() - await fetchRoleList() + if (hasPermission.value) { + await fetchRoleList() + } }) diff --git a/src/views/admin/Role.vue b/src/views/admin/Role.vue index f7f1566..6c5475e 100644 --- a/src/views/admin/Role.vue +++ b/src/views/admin/Role.vue @@ -371,6 +371,7 @@ const removePermission = async (perm) => { username, action: 'remove_permission', role_code: currentRole.value.role_code, + target_club_id: currentRole.value.club_id || '', perm_code: perm.perm_code }) if (res.code === 0) { diff --git a/src/views/member/Data.vue b/src/views/member/Data.vue index 369853c..4d4fe20 100644 --- a/src/views/member/Data.vue +++ b/src/views/member/Data.vue @@ -526,7 +526,7 @@ onMounted(fetchData) } .member-data-table :deep(.el-table td.el-table__cell) { background: rgba(12, 18, 28, 0.55) !important; - color: #d8ebff !important; + color: #ffffff !important; border-bottom: 1px solid rgba(64, 158, 255, 0.1) !important; } .member-data-table :deep(.el-table__body tr:hover > td.el-table__cell) { diff --git a/src/views/miniapp/RankReward.vue b/src/views/miniapp/RankReward.vue index 329cb0e..c2c756a 100644 --- a/src/views/miniapp/RankReward.vue +++ b/src/views/miniapp/RankReward.vue @@ -18,7 +18,7 @@

排行榜奖励配置

-

按俱乐部分奖;小程序可展示集团总榜,奖金仅发给本俱乐部用户。结算周期:昨日 / 上周 / 上月。

+

按俱乐部分奖;结算周期:昨日 / 上周 / 上月。可设置活动起止日期,超出活动期不可结算与领取。

@@ -53,6 +53,12 @@ + + + + + + @@ -151,6 +157,8 @@ const form = reactive({ sort_field: 'chengjiao_zongliang', min_metric: 0, claim_days: 30, + activity_start: '', + activity_end: '', description: '', tiers: [{ rank_from: 1, rank_to: 1, reward_amount: 0, label: '冠军' }], }) @@ -184,6 +192,8 @@ const selectScheme = () => { sort_field: hit.sort_field || defaults, min_metric: hit.min_metric || 0, claim_days: hit.claim_days || 30, + activity_start: hit.activity_start || '', + activity_end: hit.activity_end || '', description: hit.description || '', tiers: hit.tiers?.length ? hit.tiers.map((t) => ({ ...t, reward_amount: Number(t.reward_amount) })) @@ -196,6 +206,8 @@ const selectScheme = () => { sort_field: defaults, min_metric: 0, claim_days: 30, + activity_start: '', + activity_end: '', description: '', tiers: [{ rank_from: 1, rank_to: 1, reward_amount: 0, label: '冠军' }], }) diff --git a/src/views/punishment/components/FadanDetail.vue b/src/views/punishment/components/FadanDetail.vue index f78c486..f1c66ff 100644 --- a/src/views/punishment/components/FadanDetail.vue +++ b/src/views/punishment/components/FadanDetail.vue @@ -7,7 +7,7 @@ {{ fadan.shenfen_text }} ¥{{ fadan.fakuanjine }} - {{ statusText(fadan.zhuangtai) }} + {{ statusText(fadan) }} {{ fadan.yingxiang_qiangdan ? '是' : '否' }} {{ fadan.chufaliyou }} @@ -36,8 +36,14 @@
- -
+ +
+ 同意审核(进入待缴纳/申诉流程) + 驳回申请 +
+ + +
同意申诉(驳回处罚) 拒绝申诉
@@ -48,7 +54,7 @@ - + import { ref, computed, watch } from 'vue' -import { ElMessage, ElMessageBox } from 'element-plus' +import { ElMessage } from 'element-plus' import { Plus } from '@element-plus/icons-vue' +import request from '@/utils/request' import { uploadFiles } from '@/utils/upload' import { JITUAN_API } from '@/utils/club-context' +import { rowPenaltyStatusText, rowPenaltyStatusType, isPlatformAuditPending } from '@/utils/penalty-status' const props = defineProps({ visible: Boolean, @@ -90,16 +98,20 @@ const ossUrl = window.$ossURL || '' const fullZhengjuUrls = computed(() => (props.fadan?.zhengju_tupian || []).map(url => ossUrl + url)) const fullShensuUrls = computed(() => (props.fadan?.shensu_tupian || []).map(url => ossUrl + url)) -// 处理表单 const actionDialog = ref(false) const actionTitle = ref('') const currentAction = ref('') +const isPlatformAction = ref(false) const reason = ref('') const fileList = ref([]) const selectedFiles = ref([]) const submitting = ref(false) +const statusText = rowPenaltyStatusText +const statusType = rowPenaltyStatusType + const handleAction = (action) => { + isPlatformAction.value = false currentAction.value = action actionTitle.value = action === 'agree' ? '同意申诉(处罚将撤销)' : '拒绝申诉' reason.value = '' @@ -108,6 +120,16 @@ const handleAction = (action) => { actionDialog.value = true } +const handlePlatformAction = (action) => { + isPlatformAction.value = true + currentAction.value = action + actionTitle.value = action === 'platform_approve' ? '同意平台审核' : '驳回罚款申请' + reason.value = '' + fileList.value = [] + selectedFiles.value = [] + actionDialog.value = true +} + const handleFileChange = (file) => { selectedFiles.value.push(file.raw) } const handleFileRemove = (file) => { const idx = selectedFiles.value.indexOf(file.raw) @@ -117,21 +139,20 @@ const handleFileRemove = (file) => { const submitAction = async () => { submitting.value = true try { - const formData = new FormData() - formData.append('username', localStorage.getItem('username') || '') - formData.append('fadan_id', props.fadan.id) - formData.append('action', currentAction.value) - formData.append('chuli_liyou', reason.value) - selectedFiles.value.forEach(f => formData.append('tupian', f)) - - // 直接使用 request 发送亦可,这里使用 uploadFiles 并附带字段 - // 因为 uploadFiles 接受额外数据和文件,但需要注意文件字段名是 'tupian' - const res = await uploadFiles(JITUAN_API.penaltyAction, selectedFiles.value, { - username: localStorage.getItem('username') || '', + const username = localStorage.getItem('username') || '' + const payload = { + username, fadan_id: props.fadan.id, action: currentAction.value, chuli_liyou: reason.value - }, 'tupian') + } + + let res + if (isPlatformAction.value) { + res = await request.post(JITUAN_API.penaltyPlatformAudit, payload) + } else { + res = await uploadFiles(JITUAN_API.penaltyAction, selectedFiles.value, payload, 'tupian') + } if (res.code === 0) { ElMessage.success('处理成功') @@ -148,15 +169,6 @@ const submitAction = async () => { } } -const statusText = (code) => ({1:'待缴纳',2:'已缴纳',3:'申诉中',4:'申诉成功'}[code]||'未知') -const statusType = (code) => { - if (code === 1) return 'danger' - if (code === 2) return 'success' - if (code === 3) return 'warning' - if (code === 4) return 'info' - return '' -} - watch(() => props.visible, (val) => { if (!val) actionDialog.value = false }) @@ -166,5 +178,5 @@ watch(() => props.visible, (val) => { if (!val) actionDialog.value = false }) .section-title { font-weight: 600; margin-bottom: 10px; } .img-grid { display: flex; flex-wrap: wrap; gap: 10px; } .grid-img { width: 100px; height: 100px; border-radius: 8px; } -.actions { margin-top: 20px; display: flex; gap: 12px; } - \ No newline at end of file +.actions { margin-top: 20px; display: flex; flex-wrap: wrap; gap: 12px; } + diff --git a/src/views/punishment/components/FadanList.vue b/src/views/punishment/components/FadanList.vue index 5165810..bfd7864 100644 --- a/src/views/punishment/components/FadanList.vue +++ b/src/views/punishment/components/FadanList.vue @@ -8,7 +8,7 @@ @@ -35,6 +35,7 @@