feat: 合入航空账号、子账号及会员管理更新

Import the supplied management frontend, including request encryption, withdrawal updates, and vendored dependencies.
Use incoming versions for overlapping source files and retain existing environment configuration absent from the supplied directory.
This commit is contained in:
dzc committed 2026-10-29 16:52:46 +08:00
1 parent 401c7c1dd6
commit f087d6ba57
179 files changed
+21027 -9976

No files matched your search

@@ -37,3 +37,8 @@ export const approveBrokerageWithdraw = async (id: number) => {
export const rejectBrokerageWithdraw = async (data: BrokerageWithdrawVO) => {
return await request.put({ url: `/trade/brokerage-withdraw/reject`, data })
}
// 导出佣金提现 Excel
export const exportBrokerageWithdraw = async (params: any) => {
return await request.download({ url: `/trade/brokerage-withdraw/export-excel`, params })
}
+54
View File
@@ -0,0 +1,54 @@
import request from '@/config/axios'
export interface AirlineAccountVO {
id?: number
mobile: string
channel: string
password?: string
startTime: string
endTime: string
deviceId?: string
status: number
remark?: string
userId?: number
expired?: boolean
notStarted?: boolean
createTime?: string
}
export const channelOptions = [
{ value: 'CZ', label: '南方航空' },
{ value: 'MU', label: '东方航空' },
{ value: 'HU', label: '海南航空' }
]
// 渠道编码 -> 全称(列表页等处展示用;存储值仍为 CZ/MU/HU)
export const channelLabelMap: Record<string, string> = {
CZ: '南方航空',
MU: '东方航空',
HU: '海南航空'
}
export const getAirlineAccountPage = async (params: any) => {
return await request.get({ url: `/member/airline-account/page`, params })
}
export const getAirlineAccount = async (id: number) => {
return await request.get({ url: `/member/airline-account/get?id=` + id })
}
export const createAirlineAccount = async (data: AirlineAccountVO) => {
return await request.post({ url: `/member/airline-account/create`, data })
}
export const updateAirlineAccount = async (data: AirlineAccountVO) => {
return await request.put({ url: `/member/airline-account/update`, data })
}
export const deleteAirlineAccount = async (id: number) => {
return await request.delete({ url: `/member/airline-account/delete?id=` + id })
}
export const unbindAirlineDevice = async (id: number) => {
return await request.put({ url: `/member/airline-account/unbind-device?id=` + id })
}
+5
View File
@@ -37,6 +37,11 @@ export const updateUser = async (data: UserVO) => {
return await request.put({ url: `/member/user/update`, data })
}
/** 更新会员用户状态(禁用后踢出 App 登录,且无法再登录) */
export const updateUserStatus = async (data: { id: number; status: number }) => {
return await request.put({ url: `/member/user/update-status`, data })
}
// 修改会员用户等级
export const updateUserLevel = async (data: any) => {
return await request.put({ url: `/member/user/update-level`, data })
+55
View File
@@ -0,0 +1,55 @@
import request from '@/config/axios'
export interface SubAccountVO {
id: number
userId: number
username: string
nickname: string
status: number
loginDate: Date
createTime: Date
menuIds: number[]
}
export interface SubAccountMenuVO {
id: number
parentId: number
name: string
type: number
sort: number
}
// 查询子账号列表
export const getSubAccountList = () => {
return request.get({ url: '/system/sub-account/list' })
}
// 查询当前账号拥有的菜单(用于给子账号勾选)
export const getHolderMenuList = () => {
return request.get({ url: '/system/sub-account/menu-list' })
}
// 创建子账号
export const createSubAccount = (data: SubAccountVO & { password: string }) => {
return request.post({ url: '/system/sub-account/create', data })
}
// 修改子账号(昵称 + 可见菜单)
export const updateSubAccount = (userId: number, data: SubAccountVO) => {
return request.put({ url: '/system/sub-account/update?userId=' + userId, data })
}
// 修改子账号状态
export const updateSubAccountStatus = (userId: number, status: number) => {
return request.put({ url: '/system/sub-account/update-status', data: { userId, status } })
}
// 重置子账号密码
export const resetSubAccountPassword = (userId: number, password: string) => {
return request.put({ url: '/system/sub-account/reset-password', data: { userId, password } })
}
// 删除子账号
export const deleteSubAccount = (userId: number) => {
return request.delete({ url: '/system/sub-account/delete?userId=' + userId })
}
+26 -3
View File
@@ -3,11 +3,17 @@ import axios, { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestCo
import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
import qs from 'qs'
import { config } from '@/config/axios/config'
import { getAccessToken, getRefreshToken, getTenantId, removeToken, setToken } from '@/utils/auth'
import { getAccessToken, getRefreshToken, getTenantId, ensureTenantId, removeToken, setToken } from '@/utils/auth'
import errorCode from './errorCode'
import { resetRouter } from '@/router'
import { deleteUserCache } from '@/hooks/web/useCache'
import {
isEncryptedEnvelope,
needsRespDecrypt,
RESP_HEADER_ENC,
unwrapRespData
} from '@/utils/respBodyCrypto'
const tenantEnable = import.meta.env.VITE_APP_TENANT_ENABLE
const { result_code, base_url, request_timeout } = config
@@ -53,7 +59,7 @@ service.interceptors.request.use(
}
// 设置租户
if (tenantEnable && tenantEnable === 'true') {
const tenantId = getTenantId()
const tenantId = ensureTenantId()
if (tenantId) config.headers['tenant-id'] = tenantId
}
const method = config.method?.toUpperCase()
@@ -71,6 +77,10 @@ service.interceptors.request.use(
}
}
}
// 敏感接口:不传 X-Resp-Ek,由服务端在响应信封下发 ek(避免 token 刷新重试丢失 session key)
if (needsRespDecrypt(config.url)) {
config.headers[RESP_HEADER_ENC] = '1'
}
return config
},
(error: AxiosError) => {
@@ -103,6 +113,16 @@ service.interceptors.response.use(
data = await new Response(response.data).json()
}
const code = data.code || result_code
// 响应 data 为加密信封则解密(优先用响应中的 ek)
if (data?.data && isEncryptedEnvelope(data.data)) {
try {
data.data = await unwrapRespData(data.data)
} catch (e) {
console.error('响应解密失败', e)
ElMessage.error('响应解密失败,请刷新后重试')
return Promise.reject(e)
}
}
// 获取错误信息
const msg = data.msg || errorCode[code] || errorCode['default']
if (ignoreMsgs.indexOf(msg) !== -1) {
@@ -195,7 +215,10 @@ service.interceptors.response.use(
)
const refreshToken = async () => {
axios.defaults.headers.common['tenant-id'] = getTenantId()
const tenantId = ensureTenantId()
if (tenantId) {
axios.defaults.headers.common['tenant-id'] = tenantId
}
return await axios.post(base_url + '/system/auth/refresh-token?refreshToken=' + getRefreshToken())
}
const handleAuthorized = () => {
+4 -1
View File
@@ -1,7 +1,7 @@
import router from './router'
import type { RouteRecordRaw } from 'vue-router'
import { isRelogin } from '@/config/axios/service'
import { getAccessToken } from '@/utils/auth'
import { getAccessToken, ensureTenantId } from '@/utils/auth'
import { useTitle } from '@/hooks/web/useTitle'
import { useNProgress } from '@/hooks/web/useNProgress'
import { usePageLoading } from '@/hooks/web/usePageLoading'
@@ -60,6 +60,9 @@ const whiteList = [
router.beforeEach(async (to, from, next) => {
start()
loadStart()
if (import.meta.env.VITE_APP_TENANT_ENABLE === 'true') {
ensureTenantId()
}
if (getAccessToken()) {
if (to.path === '/login') {
next({ path: '/' })
+27 -4
View File
@@ -63,10 +63,33 @@ export const removeLoginForm = () => {
// ========== 租户相关 ==========
export const getTenantId = () => {
return wsCache.get(CACHE_KEY.TenantId)
const tenantEnabled = () => import.meta.env.VITE_APP_TENANT_ENABLE === 'true'
/** 环境变量配置的默认租户编号(单租户场景兜底) */
export const getDefaultTenantId = (): string | undefined => {
const id = import.meta.env.VITE_APP_DEFAULT_TENANT_ID
if (id == null || String(id).trim() === '') return undefined
return String(id).trim()
}
export const setTenantId = (username: string) => {
wsCache.set(CACHE_KEY.TenantId, username)
export const getTenantId = () => {
const cached = wsCache.get(CACHE_KEY.TenantId)
if (cached != null && cached !== '') return cached
return getDefaultTenantId()
}
/** 缓存为空时写入默认租户编号,避免已登录态请求缺少 tenant-id */
export const ensureTenantId = () => {
const current = getTenantId()
if (current == null || current === '') return undefined
if (!wsCache.get(CACHE_KEY.TenantId)) {
wsCache.set(CACHE_KEY.TenantId, current)
}
return current
}
export const setTenantId = (tenantId: string | number) => {
wsCache.set(CACHE_KEY.TenantId, tenantId)
}
export const isTenantEnabled = () => tenantEnabled()
+14 -1
View File
@@ -336,9 +336,17 @@ export const BrokerageRecordBizTypeEnum = {
type: 1,
name: '获得推广佣金'
},
WITHDRAW: {
OPEN_TYPE: {
type: 2,
name: '获得会员佣金'
},
WITHDRAW: {
type: 3,
name: '提现申请'
},
WITHDRAW_REJECT: {
type: 4,
name: '提现申请驳回'
}
}
/**
@@ -385,6 +393,11 @@ export const BrokerageWithdrawTypeEnum = {
ALIPAY: {
type: 4,
name: '支付宝'
},
/** 业务侧实际在用的支付宝提现类型(历史常量名 WECHAT_API) */
ALIPAY_API: {
type: 5,
name: '支付宝'
}
}
+156
View File
@@ -0,0 +1,156 @@
import forge from 'node-forge'
/** 与后端 RespBodyEncryptSupport.PATH_SUFFIXES 对齐 */
const PATH_SUFFIXES = [
'/member/recent/quick-page',
'/member/recent/getById',
'/member/recent/look-wechat',
'/member/user/page'
]
const RESP_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtSt5lLsmloOnhFHdh8NJ
a1L76U/yQWX+S2VPyyXgp7/9TNcbOlygQ0iD0+SXd828wrVkeHo67ELtcsLF/7Sd
eQ1mykvxEswKhAG7SCjaocq9SwMnSre7iHnrtnNKRRH8Sozds67sRJPePz8o8W/Z
r6XKeEuStvZ3E6n2HD7Ug455qIyFMDJnY7ntb3AEWnGdhXsF9Nqi22yuqJrLQOMp
BWu/pTFhpCPr2ECdMdulG491J+snR/UYJZu7Ro65O4Fb3d4EVC9EHKH2cQDLAkVW
ceFliYvihrutYvBkg9aygt5FaQZiUrfie0iVy5FvZLkO3nv+fm9lPTKNyadPb26k
DwIDAQAB
-----END PUBLIC KEY-----`
export const RESP_HEADER_ENC = 'X-Resp-Enc'
export const RESP_HEADER_EK = 'X-Resp-Ek'
export const RESP_AES_KEY = 'respAesKey'
let _publicKey: forge.pki.rsa.PublicKey | null = null
function getPublicKey(): forge.pki.rsa.PublicKey {
if (!_publicKey) {
_publicKey = forge.pki.publicKeyFromPem(RESP_PUBLIC_KEY)
}
return _publicKey
}
function bytesToBinary(bytes: Uint8Array): string {
let binary = ''
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i])
}
return binary
}
function binaryToBytes(binary: string): Uint8Array {
const out = new Uint8Array(binary.length)
for (let i = 0; i < binary.length; i++) {
out[i] = binary.charCodeAt(i)
}
return out
}
export function needsRespDecrypt(url?: string): boolean {
if (!url) return false
let path = url.split('?')[0]
path = path.replace(/^https?:\/\/[^/]+/i, '')
if (path.startsWith('/admin-api')) {
path = path.slice('/admin-api'.length)
}
if (!path.startsWith('/')) {
path = `/${path}`
}
return PATH_SUFFIXES.some((suffix) => path.endsWith(suffix))
}
function randomBytes(length: number): Uint8Array {
const buf = new Uint8Array(length)
crypto.getRandomValues(buf)
return buf
}
/** RSA/PKCS1 公钥加密 AES Key(与 Java 服务端 decryptAesKey 对齐) */
function rsaEncryptAesKey(aesKey: Uint8Array): string {
const encrypted = getPublicKey().encrypt(bytesToBinary(aesKey), 'RSAES-PKCS1-V1_5')
return forge.util.encode64(encrypted)
}
/** 解析 PKCS#1 v1.5 type-1 填充(Java 私钥 ENCRYPT_MODE 下发 ek) */
function pkcs1V15Type1Message(buf: Uint8Array): Uint8Array {
if (buf.length >= 2 && buf[0] === 0 && buf[1] === 1) {
let i = 2
while (i < buf.length && buf[i] === 0xff) i++
if (i < buf.length && buf[i] === 0) {
return buf.slice(i + 1)
}
}
if (buf.length === 32) return buf
if (buf.length > 32) return buf.slice(buf.length - 32)
throw new Error('RSA 解密 ek 后长度非法')
}
/** 服务端私钥加密下发的 ek,公钥解密(Java Cipher.ENCRYPT_MODE + privateKey) */
function rsaPublicDecryptEk(ekBase64: string): Uint8Array {
const encrypted = forge.util.decode64(ekBase64)
const raw = forge.pki.rsa.decrypt(encrypted, getPublicKey(), true, false)
return pkcs1V15Type1Message(binaryToBytes(raw))
}
/** 请求前:生成 AES Key 并 RSA 加密写入 Header */
export function prepareRespEncryptSession(): { aesKey: Uint8Array; ekBase64: string } {
const aesKey = randomBytes(32)
return { aesKey, ekBase64: rsaEncryptAesKey(aesKey) }
}
function base64ToBytes(b64: string): Uint8Array {
const binary = atob(b64)
const bytes = new Uint8Array(binary.length)
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i)
}
return bytes
}
/** 纯 JS AES-GCM,HTTP 非安全上下文下 crypto.subtle 不可用 */
function aesGcmDecrypt(key: Uint8Array, iv: Uint8Array, ctWithTag: Uint8Array): Uint8Array {
const tagLen = 16
const ct = ctWithTag.slice(0, ctWithTag.length - tagLen)
const tag = ctWithTag.slice(ctWithTag.length - tagLen)
const forgeKey = forge.util.createBuffer(bytesToBinary(key))
const decipher = forge.cipher.createDecipher('AES-GCM', forgeKey)
decipher.start({
iv: forge.util.createBuffer(bytesToBinary(iv)),
tag: forge.util.createBuffer(bytesToBinary(tag))
})
decipher.update(forge.util.createBuffer(bytesToBinary(ct)))
decipher.finish()
if (decipher.output.length() === 0) {
throw new Error('AES-GCM 解密失败')
}
return binaryToBytes(decipher.output.getBytes())
}
export function isEncryptedEnvelope(data: unknown): data is Record<string, unknown> {
if (!data || typeof data !== 'object') return false
const enc = (data as Record<string, unknown>).enc
return Number(enc) === 1
}
/** 解密 CommonResult.data 信封(依赖响应中的 ek,或传入请求阶段缓存的 aesKey) */
export async function unwrapRespData(data: unknown, aesKey?: Uint8Array): Promise<unknown> {
if (!isEncryptedEnvelope(data)) return data
const envelope = data as Record<string, unknown>
let key = aesKey
if (!key) {
const ek = envelope.ek
if (typeof ek !== 'string' || !ek) {
throw new Error('加密响应缺少 ek,请升级客户端')
}
key = rsaPublicDecryptEk(ek)
}
const ivB64 = envelope.iv
const ctB64 = envelope.ct
if (typeof ivB64 !== 'string' || typeof ctB64 !== 'string') {
throw new Error('加密响应缺少 iv/ct')
}
const plain = aesGcmDecrypt(key, base64ToBytes(ivB64), base64ToBytes(ctB64))
const text = new TextDecoder().decode(plain)
return JSON.parse(text)
}
+20 -8
View File
@@ -167,10 +167,18 @@ const getCode = async () => {
}
// 获取租户 ID
const getTenantId = async () => {
if (loginData.tenantEnable === 'true') {
const res = await LoginApi.getTenantIdByName(loginData.loginForm.tenantName)
authUtil.setTenantId(res)
if (loginData.tenantEnable !== 'true') return
const name = loginData.loginForm.tenantName?.trim()
if (name) {
try {
const res = await LoginApi.getTenantIdByName(name)
authUtil.setTenantId(res)
return
} catch (_) {
// 租户名解析失败时走默认租户编号
}
}
authUtil.ensureTenantId()
}
// 记住我
const getLoginFormCache = () => {
@@ -188,11 +196,15 @@ const getLoginFormCache = () => {
// 根据域名,获得租户信息
const getTenantByWebsite = async () => {
const website = location.host
const res = await LoginApi.getTenantByWebsite(website)
if (res) {
loginData.loginForm.tenantName = res.name
authUtil.setTenantId(res.id)
}
try {
const res = await LoginApi.getTenantByWebsite(website)
if (res) {
loginData.loginForm.tenantName = res.name
authUtil.setTenantId(res.id)
return
}
} catch (_) {}
authUtil.ensureTenantId()
}
const loading = ref() // ElLoading.service 返回的实例
// 登录
@@ -25,7 +25,7 @@
<el-col :span="24">
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" :key="dict.value"
:value="dict.value">
{{ dict.label }}
@@ -34,11 +34,13 @@ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" :key="dict.value"
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item v-if="false" label="位置" prop="position">
<el-form-item label="位置" prop="position">
<el-radio-group v-model="formData.position">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.PROMOTION_BANNER_POSITION)" :key="dict.value"
:value="dict.value">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.PROMOTION_BANNER_POSITION)"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
@@ -73,7 +75,7 @@ const formData = ref({
title: undefined,
picUrl: undefined,
status: 0,
position: 1,
position: 6, // 默认动态页(App 动态 banner)
url: undefined,
sort: 0,
memo: undefined
@@ -117,7 +119,6 @@ const submitForm = async () => {
formLoading.value = true
try {
const data = formData.value as unknown as BannerApi.BannerVO
data.position = 6
if (formType.value === 'create') {
await BannerApi.createBanner(data)
message.success(t('common.createSuccess'))
@@ -140,7 +141,7 @@ const resetForm = () => {
title: undefined,
picUrl: undefined,
status: 0,
position: 1,
position: 6, // 默认动态页(App 动态 banner)
url: undefined,
sort: 0,
memo: undefined
+196 -171
View File
@@ -1,171 +1,196 @@
<template>
<doc-alert title="【交易】分销返佣" url="https://doc.iocoder.cn/mall/trade-brokerage/" />
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="用户编号" prop="userId">
<el-input
v-model="queryParams.userId"
placeholder="请输入用户编号"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="业务类型" prop="bizType">
<el-select
v-model="queryParams.bizType"
placeholder="请选择业务类型"
clearable
class="!w-240px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.BROKERAGE_RECORD_BIZ_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable class="!w-240px">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.BROKERAGE_RECORD_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="编号" align="center" prop="id" min-width="60" />
<el-table-column label="用户编号" align="center" prop="userId" min-width="80" />
<el-table-column label="头像" align="center" prop="userAvatar" width="70px">
<template #default="scope">
<el-avatar :src="scope.row.userAvatar" />
</template>
</el-table-column>
<el-table-column label="昵称" align="center" prop="userNickname" min-width="80px" />
<el-table-column label="业务类型" align="center" prop="bizType" min-width="85">
<template #default="scope">
<dict-tag :type="DICT_TYPE.BROKERAGE_RECORD_BIZ_TYPE" :value="scope.row.bizType" />
</template>
</el-table-column>
<el-table-column label="业务编号" align="center" prop="bizId" min-width="80" />
<el-table-column label="标题" align="center" prop="title" min-width="110" />
<el-table-column
label="金额"
align="center"
prop="price"
min-width="60"
:formatter="fenToYuanFormat"
/>
<el-table-column label="说明" align="center" prop="description" min-width="120" />
<el-table-column label="状态" align="center" prop="status" min-width="85">
<template #default="scope">
<dict-tag :type="DICT_TYPE.BROKERAGE_RECORD_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column
label="解冻时间"
align="center"
prop="unfreezeTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180px"
/>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
</template>
<script setup lang="ts">
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import * as BrokerageRecordApi from '@/api/mall/trade/brokerage/record'
import { fenToYuanFormat } from '@/utils/formatter'
defineOptions({ name: 'TradeBrokerageRecord' })
const loading = ref(true) // 列表的加载中
const total = ref(0) // 列表的总页数
const list = ref([]) // 列表的数据
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
userId: null,
bizType: null,
price: null,
status: null,
createTime: []
})
const queryFormRef = ref() // 搜索的表单
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await BrokerageRecordApi.getBrokerageRecordPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>
<template>
<doc-alert title="【交易】分销返佣" url="https://doc.iocoder.cn/mall/trade-brokerage/" />
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="用户编号" prop="userId">
<el-input
v-model="queryParams.userId"
placeholder="请输入用户编号"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable class="!w-240px">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.BROKERAGE_RECORD_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="编号" align="center" prop="id" min-width="60" />
<el-table-column label="用户编号" align="center" prop="userId" min-width="80" />
<el-table-column label="头像" align="center" prop="userAvatar" width="70px">
<template #default="scope">
<el-avatar :src="scope.row.userAvatar" />
</template>
</el-table-column>
<el-table-column label="昵称" align="center" prop="userNickname" min-width="80px" />
<el-table-column label="业务编号" align="center" prop="bizId" min-width="80" />
<el-table-column label="标题" align="center" prop="title" min-width="110" />
<el-table-column
label="金额"
align="center"
prop="price"
min-width="60"
:formatter="fenToYuanFormat"
/>
<el-table-column label="说明" align="center" prop="description" min-width="120" />
<el-table-column label="状态" align="center" prop="status" min-width="85">
<template #default="scope">
<dict-tag :type="DICT_TYPE.BROKERAGE_RECORD_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column
label="解冻时间"
align="center"
prop="unfreezeTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180px"
/>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="applyPage"
/>
</ContentWrap>
</template>
<script setup lang="ts">
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import * as BrokerageRecordApi from '@/api/mall/trade/brokerage/record'
import { fenToYuanFormat } from '@/utils/formatter'
import { BrokerageRecordBizTypeEnum } from '@/utils/constants'
defineOptions({ name: 'TradeBrokerageRecord' })
const loading = ref(true)
const total = ref(0)
const list = ref<any[]>([])
const allCommissionList = ref<any[]>([])
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
userId: null as number | null,
status: null as number | null,
createTime: [] as string[]
})
const queryFormRef = ref()
/** 仅保留分佣入账(排除提现申请/驳回;兼容历史 bizType 错位) */
const isCommissionRecord = (row: any) => {
if (!row) return false
if (
row.bizType === BrokerageRecordBizTypeEnum.WITHDRAW.type ||
row.bizType === BrokerageRecordBizTypeEnum.WITHDRAW_REJECT.type
) {
return false
}
const title = String(row.title || '')
const description = String(row.description || '')
if (title.includes('提现') || description.includes('提现')) {
return false
}
return Number(row.price) > 0
}
const applyPage = () => {
total.value = allCommissionList.value.length
const start = (queryParams.pageNo - 1) * queryParams.pageSize
list.value = allCommissionList.value.slice(start, start + queryParams.pageSize)
}
/** 查询列表:分页拉取后过滤提现,只展示分佣 */
const getList = async () => {
loading.value = true
try {
const fetchSize = 100 // 后端 PageParam 上限
const maxPages = 20
const collected: any[] = []
let remoteTotal = 0
for (let pageNo = 1; pageNo <= maxPages; pageNo++) {
const data = await BrokerageRecordApi.getBrokerageRecordPage({
userId: queryParams.userId,
status: queryParams.status,
createTime: queryParams.createTime,
pageNo,
pageSize: fetchSize
})
const rows = data.list || []
remoteTotal = data.total || 0
collected.push(...rows)
if (rows.length < fetchSize || collected.length >= remoteTotal) {
break
}
}
allCommissionList.value = collected.filter(isCommissionRecord)
// 当前页超出时回退到最后一页
const maxPage = Math.max(1, Math.ceil(allCommissionList.value.length / queryParams.pageSize) || 1)
if (queryParams.pageNo > maxPage) {
queryParams.pageNo = maxPage
}
applyPage()
} finally {
loading.value = false
}
}
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
onMounted(() => {
getList()
})
</script>
+244 -268
View File
@@ -1,268 +1,244 @@
<template>
<doc-alert title="【交易】分销返佣" url="https://doc.iocoder.cn/mall/trade-brokerage/" />
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="用户编号" prop="userId">
<el-input
v-model="queryParams.userId"
placeholder="请输入用户编号"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="提现类型" prop="type">
<el-select
v-model="queryParams.type"
placeholder="请选择提现类型"
clearable
class="!w-240px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.BROKERAGE_WITHDRAW_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="账号" prop="accountNo">
<el-input
v-model="queryParams.accountNo"
placeholder="请输入账号"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="提现银行" prop="bankName">
<el-select
v-model="queryParams.bankName"
placeholder="请选择提现银行"
clearable
class="!w-240px"
>
<el-option
v-for="dict in getStrDictOptions(DICT_TYPE.BROKERAGE_BANK_NAME)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable class="!w-240px">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.BROKERAGE_WITHDRAW_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="申请时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="编号" align="left" prop="id" min-width="60px" />
<el-table-column label="用户信息" align="left" min-width="120px">
<template #default="scope">
<div>编号:{{ scope.row.userId }}</div>
<div>昵称:{{ scope.row.userNickname }}</div>
</template>
</el-table-column>
<el-table-column label="提现金额" align="left" prop="price" min-width="80px">
<template #default="scope">
<div>金 额:¥{{ fenToYuan(scope.row.price) }}</div>
<div>手续费:¥{{ fenToYuan(scope.row.feePrice) }}</div>
</template>
</el-table-column>
<el-table-column label="提现方式" align="left" prop="type" min-width="120px">
<template #default="scope">
<div v-if="scope.row.type === BrokerageWithdrawTypeEnum.WALLET.type"> 余额 </div>
<div v-else>
{{ getDictLabel(DICT_TYPE.BROKERAGE_WITHDRAW_TYPE, scope.row.type) }}
<span v-if="scope.row.accountNo">账号:{{ scope.row.accountNo }}</span>
</div>
<template v-if="scope.row.type === BrokerageWithdrawTypeEnum.BANK.type">
<div>真实姓名:{{ scope.row.name }}</div>
<div>
银行名称:
<dict-tag :type="DICT_TYPE.BROKERAGE_BANK_NAME" :value="scope.row.bankName" />
</div>
<div>开户地址:{{ scope.row.bankAddress }}</div>
</template>
</template>
</el-table-column>
<el-table-column label="收款码" align="left" prop="accountQrCodeUrl" min-width="70px">
<template #default="scope">
<el-image
v-if="scope.row.accountQrCodeUrl"
:src="scope.row.accountQrCodeUrl"
class="h-40px w-40px"
:preview-src-list="[scope.row.accountQrCodeUrl]"
preview-teleported
/>
<span v-else>无</span>
</template>
</el-table-column>
<el-table-column
label="申请时间"
align="left"
prop="createTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="备注" align="left" prop="remark" />
<el-table-column label="状态" align="left" prop="status" min-width="120px">
<template #default="scope">
<dict-tag :type="DICT_TYPE.BROKERAGE_WITHDRAW_STATUS" :value="scope.row.status" />
<div v-if="scope.row.auditTime" class="text-xs">
时间:{{ formatDate(scope.row.auditTime) }}
</div>
<div v-if="scope.row.auditReason" class="text-xs">
原因:{{ scope.row.auditReason }}
</div>
</template>
</el-table-column>
<el-table-column label="操作" align="left" width="110px" fixed="right">
<template #default="scope">
<template v-if="scope.row.status === BrokerageWithdrawStatusEnum.AUDITING.status">
<el-button
link
type="primary"
@click="handleApprove(scope.row.id)"
v-hasPermi="['trade:brokerage-withdraw:audit']"
>
通过
</el-button>
<el-button
link
type="danger"
@click="openForm(scope.row.id)"
v-hasPermi="['trade:brokerage-withdraw:audit']"
>
驳回
</el-button>
</template>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗:添加/修改 -->
<BrokerageWithdrawRejectForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { DICT_TYPE, getDictLabel, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
import { dateFormatter, formatDate } from '@/utils/formatTime'
import * as BrokerageWithdrawApi from '@/api/mall/trade/brokerage/withdraw'
import BrokerageWithdrawRejectForm from './BrokerageWithdrawRejectForm.vue'
import { BrokerageWithdrawStatusEnum, BrokerageWithdrawTypeEnum } from '@/utils/constants'
import { fenToYuanFormat } from '@/utils/formatter'
import { fenToYuan } from '@/utils'
defineOptions({ name: 'BrokerageWithdraw' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const loading = ref(true) // 列表的加载中
const total = ref(0) // 列表的总页数
const list = ref([]) // 列表的数据
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
userId: null,
type: null,
name: null,
accountNo: null,
bankName: null,
status: null,
auditReason: null,
auditTime: [],
remark: null,
createTime: []
})
const queryFormRef = ref() // 搜索的表单
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await BrokerageWithdrawApi.getBrokerageWithdrawPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (id: number) => {
formRef.value.open(id)
}
/** 审核通过 */
const handleApprove = async (id: number) => {
try {
loading.value = true
await message.confirm('确定要审核通过吗?')
await BrokerageWithdrawApi.approveBrokerageWithdraw(id)
await message.success(t('common.success'))
await getList()
} finally {
loading.value = false
}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>
<template>
<doc-alert title="【交易】分销返佣" url="https://doc.iocoder.cn/mall/trade-brokerage/" />
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="用户编号" prop="userId">
<el-input
v-model="queryParams.userId"
placeholder="请输入用户编号"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="支付宝账号" prop="accountNo" label-width="90px">
<el-input
v-model="queryParams.accountNo"
placeholder="请输入支付宝账号"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable class="!w-240px">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.BROKERAGE_WITHDRAW_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="申请时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="success"
plain
:loading="exportLoading"
@click="handleExport"
v-hasPermi="['trade:brokerage-withdraw:query', 'trade:brokerage-withdraw:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出 Excel
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="编号" align="left" prop="id" min-width="60px" />
<el-table-column label="用户信息" align="left" min-width="120px">
<template #default="scope">
<div>编号:{{ scope.row.userId }}</div>
<div>昵称:{{ scope.row.userNickname }}</div>
</template>
</el-table-column>
<el-table-column label="提现金额" align="left" prop="price" min-width="80px">
<template #default="scope">
<div>金 额:¥{{ fenToYuan(scope.row.price) }}</div>
<div>手续费:¥{{ fenToYuan(scope.row.feePrice) }}</div>
</template>
</el-table-column>
<el-table-column label="提现方式" align="left" prop="type" min-width="140px">
<template #default="scope">
<div>支付宝</div>
<div v-if="scope.row.accountNo">账号:{{ scope.row.accountNo }}</div>
<div v-if="scope.row.name">姓名:{{ scope.row.name }}</div>
</template>
</el-table-column>
<el-table-column label="收款码" align="left" prop="accountQrCodeUrl" min-width="70px">
<template #default="scope">
<el-image
v-if="scope.row.accountQrCodeUrl"
:src="scope.row.accountQrCodeUrl"
class="h-40px w-40px"
:preview-src-list="[scope.row.accountQrCodeUrl]"
preview-teleported
/>
<span v-else>无</span>
</template>
</el-table-column>
<el-table-column
label="申请时间"
align="left"
prop="createTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="备注" align="left" prop="remark" />
<el-table-column label="状态" align="left" prop="status" min-width="120px">
<template #default="scope">
<dict-tag :type="DICT_TYPE.BROKERAGE_WITHDRAW_STATUS" :value="scope.row.status" />
<div v-if="scope.row.auditTime" class="text-xs">
时间:{{ formatDate(scope.row.auditTime) }}
</div>
<div v-if="scope.row.auditReason" class="text-xs">
原因:{{ scope.row.auditReason }}
</div>
</template>
</el-table-column>
<el-table-column label="操作" align="left" width="110px" fixed="right">
<template #default="scope">
<template v-if="scope.row.status === BrokerageWithdrawStatusEnum.AUDITING.status">
<el-button
link
type="primary"
@click="handleApprove(scope.row.id)"
v-hasPermi="['trade:brokerage-withdraw:audit']"
>
通过
</el-button>
<el-button
link
type="danger"
@click="openForm(scope.row.id)"
v-hasPermi="['trade:brokerage-withdraw:audit']"
>
驳回
</el-button>
</template>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗:添加/修改 -->
<BrokerageWithdrawRejectForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter, formatDate } from '@/utils/formatTime'
import download from '@/utils/download'
import * as BrokerageWithdrawApi from '@/api/mall/trade/brokerage/withdraw'
import BrokerageWithdrawRejectForm from './BrokerageWithdrawRejectForm.vue'
import { BrokerageWithdrawStatusEnum } from '@/utils/constants'
import { fenToYuan } from '@/utils'
defineOptions({ name: 'BrokerageWithdraw' })
const { t } = useI18n()
const message = useMessage()
const loading = ref(true)
const exportLoading = ref(false)
const total = ref(0)
const list = ref([])
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
userId: null,
type: null,
name: null,
accountNo: null,
bankName: null,
status: null,
auditReason: null,
auditTime: [],
remark: null,
createTime: []
})
const queryFormRef = ref()
const getList = async () => {
loading.value = true
try {
const data = await BrokerageWithdrawApi.getBrokerageWithdrawPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
const formRef = ref()
const openForm = (id: number) => {
formRef.value.open(id)
}
const handleApprove = async (id: number) => {
try {
loading.value = true
await message.confirm('确定要审核通过吗?')
await BrokerageWithdrawApi.approveBrokerageWithdraw(id)
await message.success(t('common.success'))
await getList()
} finally {
loading.value = false
}
}
const handleExport = async () => {
try {
await message.exportConfirm()
exportLoading.value = true
const data = await BrokerageWithdrawApi.exportBrokerageWithdraw(queryParams)
download.excel(data, '佣金提现.xls')
} catch {
} finally {
exportLoading.value = false
}
}
onMounted(() => {
getList()
})
</script>
@@ -0,0 +1,172 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible" width="560px">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="110px"
v-loading="formLoading"
>
<el-form-item label="手机号" prop="mobile">
<el-input v-model="formData.mobile" placeholder="请输入手机号" maxlength="11" />
</el-form-item>
<el-form-item label="品牌渠道" prop="channel">
<el-select v-model="formData.channel" placeholder="请选择品牌渠道" class="!w-full">
<el-option
v-for="opt in AirlineAccountApi.channelOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input
v-model="formData.password"
:placeholder="formType === 'create' ? '请输入密码' : '不填则不修改密码'"
show-password
maxlength="16"
/>
</el-form-item>
<el-form-item label="开始时间" prop="startTime">
<el-date-picker
v-model="formData.startTime"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="选择开始时间"
class="!w-full"
/>
</el-form-item>
<el-form-item label="结束时间" prop="endTime">
<el-date-picker
v-model="formData.endTime"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="选择结束时间"
class="!w-full"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio :value="0">开启</el-radio>
<el-radio :value="1">关闭</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" type="textarea" placeholder="可选备注" />
</el-form-item>
<el-form-item v-if="formType === 'update' && formData.deviceId" label="已绑设备">
<el-input v-model="formData.deviceId" disabled />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
<el-button @click="dialogVisible = false">取 消</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import * as AirlineAccountApi from '@/api/member/airlineAccount'
const { t } = useI18n()
const message = useMessage()
const dialogVisible = ref(false)
const dialogTitle = ref('')
const formLoading = ref(false)
const formType = ref('')
const formData = ref({
id: undefined as number | undefined,
mobile: undefined as string | undefined,
channel: undefined as string | undefined,
password: undefined as string | undefined,
startTime: undefined as string | undefined,
endTime: undefined as string | undefined,
status: 0,
remark: undefined as string | undefined,
deviceId: undefined as string | undefined
})
const formRules = reactive({
mobile: [{ required: true, message: '手机号不能为空', trigger: 'blur' }],
channel: [{ required: true, message: '品牌渠道不能为空', trigger: 'change' }],
password: [
{
validator: (_rule: any, value: string, callback: any) => {
if (formType.value === 'create' && !value) {
callback(new Error('密码不能为空'))
return
}
if (value && (value.length < 4 || value.length > 16)) {
callback(new Error('密码长度为 4-16 位'))
return
}
callback()
},
trigger: 'blur'
}
],
startTime: [{ required: true, message: '开始时间不能为空', trigger: 'change' }],
endTime: [{ required: true, message: '结束时间不能为空', trigger: 'change' }],
status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
})
const formRef = ref()
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
if (id) {
formLoading.value = true
try {
formData.value = await AirlineAccountApi.getAirlineAccount(id)
formData.value.password = undefined
} finally {
formLoading.value = false
}
}
}
defineExpose({ open })
const emit = defineEmits(['success'])
const submitForm = async () => {
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
formLoading.value = true
try {
const data = { ...formData.value } as AirlineAccountApi.AirlineAccountVO
if (formType.value === 'create') {
await AirlineAccountApi.createAirlineAccount(data)
message.success(t('common.createSuccess'))
} else {
if (!data.password) {
delete data.password
}
await AirlineAccountApi.updateAirlineAccount(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
emit('success')
} finally {
formLoading.value = false
}
}
const resetForm = () => {
formData.value = {
id: undefined,
mobile: undefined,
channel: undefined,
password: undefined,
startTime: undefined,
endTime: undefined,
status: 0,
remark: undefined,
deviceId: undefined
}
formRef.value?.resetFields()
}
</script>
+199
View File
@@ -0,0 +1,199 @@
<template>
<ContentWrap>
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="88px"
>
<el-form-item label="手机号" prop="mobile">
<el-input
v-model="queryParams.mobile"
placeholder="请输入手机号"
clearable
class="!w-240px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="品牌渠道" prop="channel">
<el-select v-model="queryParams.channel" placeholder="全部" clearable class="!w-240px">
<el-option
v-for="opt in AirlineAccountApi.channelOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="全部" clearable class="!w-240px">
<el-option label="开启" :value="0" />
<el-option label="关闭" :value="1" />
</el-select>
</el-form-item>
<el-form-item label="设备号" prop="deviceId">
<el-input
v-model="queryParams.deviceId"
placeholder="设备唯一识别号"
clearable
class="!w-240px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
@click="openForm('create')"
v-hasPermi="['member:airline-account:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="编号" align="center" prop="id" width="80" />
<el-table-column label="手机号" align="center" prop="mobile" width="130" />
<el-table-column label="品牌渠道" align="center" prop="channel" width="100">
<template #default="scope">
<el-tag>{{ AirlineAccountApi.channelLabelMap[scope.row.channel] || scope.row.channel }}</el-tag>
</template>
</el-table-column>
<el-table-column label="开始时间" align="center" prop="startTime" :formatter="dateFormatter" width="180" />
<el-table-column label="结束时间" align="center" prop="endTime" :formatter="dateFormatter" width="180" />
<el-table-column label="有效期" align="center" width="100">
<template #default="scope">
<el-tag v-if="scope.row.notStarted" type="info">未生效</el-tag>
<el-tag v-else-if="scope.row.expired" type="danger">已到期</el-tag>
<el-tag v-else type="success">有效</el-tag>
</template>
</el-table-column>
<el-table-column label="设备识别号" align="center" prop="deviceId" min-width="160">
<template #default="scope">
<span>{{ scope.row.deviceId || '未绑定' }}</span>
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status" width="90">
<template #default="scope">
<el-tag :type="scope.row.status === 0 ? 'success' : 'danger'">
{{ scope.row.status === 0 ? '开启' : '关闭' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" min-width="120" />
<el-table-column label="创建时间" align="center" prop="createTime" :formatter="dateFormatter" width="180" />
<el-table-column label="操作" align="center" width="240" fixed="right">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['member:airline-account:update']"
>
编辑
</el-button>
<el-button
link
type="warning"
@click="handleUnbind(scope.row.id)"
v-hasPermi="['member:airline-account:unbind']"
:disabled="!scope.row.deviceId"
>
解绑设备
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['member:airline-account:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<AirlineAccountForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts" name="MemberAirlineAccount">
import { dateFormatter } from '@/utils/formatTime'
import * as AirlineAccountApi from '@/api/member/airlineAccount'
import AirlineAccountForm from './AirlineAccountForm.vue'
const message = useMessage()
const { t } = useI18n()
const loading = ref(true)
const total = ref(0)
const list = ref([])
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
mobile: undefined,
channel: undefined,
status: undefined,
deviceId: undefined
})
const queryFormRef = ref()
const getList = async () => {
loading.value = true
try {
const data = await AirlineAccountApi.getAirlineAccountPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
const resetQuery = () => {
queryFormRef.value?.resetFields()
handleQuery()
}
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
const handleDelete = async (id: number) => {
try {
await message.delConfirm()
await AirlineAccountApi.deleteAirlineAccount(id)
message.success(t('common.delSuccess'))
await getList()
} catch {}
}
const handleUnbind = async (id: number) => {
try {
await message.confirm('解绑后该账号可在新手机上重新登录绑定,是否继续?')
await AirlineAccountApi.unbindAirlineDevice(id)
message.success('解绑成功')
await getList()
} catch {}
}
onMounted(() => {
getList()
})
</script>
+5 -70
View File
@@ -1,27 +1,7 @@
<template>
<ContentWrap>
<el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
<el-form-item label="审核类型" prop="bizType">
<el-select v-model="queryParams.bizType" placeholder="请选择审核类型" clearable class="!w-240px">
<el-option
v-for="dict in getStrDictOptions(DICT_TYPE.MEMBER_COM_AUTH_TYPE)" :key="dict.value"
:label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="审核状态" prop="authStatus">
<el-select v-model="queryParams.authStatus" placeholder="请选择审核状态" clearable class="!w-240px">
<el-option
v-for="dict in getStrDictOptions(DICT_TYPE.MEMBER_COM_AUTH_STATUS)" :key="dict.value"
:label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form class="-mb-15px" :inline="true" label-width="68px">
<el-form-item>
<el-button @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" /> 搜索
</el-button>
<el-button @click="resetQuery">
<Icon icon="ep:refresh" class="mr-5px" /> 重置
</el-button>
<el-button type="primary" @click="openForm('create')" v-hasPermi="['member:level:create']">
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
@@ -59,20 +39,7 @@ v-for="dict in getStrDictOptions(DICT_TYPE.MEMBER_COM_AUTH_STATUS)" :key="dict.v
<dict-tag :type="DICT_TYPE.MEMBER_COM_AUTH_STATUS" :value="row.authStatus" />
</template>
</el-table-column>
<el-table-column label="拒绝原因" align="center" prop="failReason" min-width="150" />
<el-table-column label="申请时间" align="center" prop="createTime" :formatter="dateFormatter" min-width="170" />
<el-table-column label="操作" align="center" min-width="110px" fixed="right">
<template #default="{ row }">
<el-button v-if="[3, 4].includes(row.bizType)" link type="primary" @click="openForm('view', row)">
查看
</el-button>
<el-button
v-if="![3, 4].includes(row.bizType) && !['3', '4'].includes(row.authStatus)" link type="primary"
@click="openForm('check', row)">
审核
</el-button>
</template>
</el-table-column>
</el-table>
<Pagination
v-model:limit="queryParams.pageSize" v-model:page="queryParams.pageNo" :total="total"
@@ -82,27 +49,19 @@ v-model:limit="queryParams.pageSize" v-model:page="queryParams.pageNo" :total="t
</template>
<script setup lang="ts">
import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
import { DICT_TYPE } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import * as MemberAuthApi from '@/api/member/com-auth'
import ComAuthForm from './cmp/com-auth-form.vue'
const message = useMessage()
const { t } = useI18n()
const queryFormRef = ref()
const loading = ref(true)
const total = ref(0)
const list = ref([])
const queryParams = reactive<any>({
pageNo: 1,
pageSize: 10,
name: null,
status: null,
bizType: null,
authStatus: null,
})
/** 查询列表 */
@@ -117,34 +76,10 @@ const getList = async () => {
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, row: any) => {
formRef.value.open(type, row.id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await MemberAuthApi.AuthApi.deleteAuth(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch { }
const openForm = (type: string, row?: any) => {
formRef.value.open(type, row?.id)
}
/** 初始化 **/
@@ -174,4 +109,4 @@ onMounted(() => {
color: lightblue;
}
}
</style>
</style>
+219 -240
View File
@@ -1,240 +1,219 @@
<template>
<doc-alert title="会员手册(功能开启)" url="https://doc.iocoder.cn/member/build/" />
<ContentWrap>
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="120px" v-loading="formLoading">
<el-form-item label="hideId" v-show="false">
<el-input v-model="formData.id" />
</el-form-item>
<el-tabs>
<el-tab-pane label="聊天">
<!-- start 普通用户 start -->
<el-form-item label-width="200px" label="普通用户每天沟通人数" prop="pointTradeDeductUnitPrice">
<el-input-number v-model="formData.defaultChatUserCout" placeholder="" :min="-1" :max="100" />
<el-text class="w-full" size="small" type="info">
<div>
<span>普通用户</span>
<span class="color-red">(非会员)</span>
一天可以免费沟通
<span class="color-blue">
{{ formData.defaultChatUserCout == -1 ? '无限' : formData.defaultChatUserCout }}
</span>
<span>人</span>
</div>
</el-text>
</el-form-item>
<el-form-item label-width="200px" label="普通用户每天沟通次数" prop="pointTradeDeductUnitPrice">
<el-input-number v-model="formData.defaultChatUserMsgCout" placeholder="" :min="-1" :max="100" />
<el-text class="w-full" size="small" type="info">
<div>
<span>普通用户</span>
<span class="color-red">(非会员)</span>
一天可以免费和其他用户发送
<span class="color-blue">
{{ formData.defaultChatUserMsgCout == -1 ? '无限' : formData.defaultChatUserMsgCout }}
</span>
<span>条消息</span>
</div>
</el-text>
</el-form-item>
<!-- end 普通用户 end -->
<!-- start VIP用户 start -->
<el-form-item label-width="200px" label="VIP用户每天沟通人数" prop="pointTradeDeductUnitPrice">
<el-input-number v-model="formData.vipChatUserCout" placeholder="" :min="-1" :max="100" />
<el-text class="w-full" size="small" type="info">
<div>
<span>VIP用户</span>
<span class="color-red">(会员)</span>
一天可以沟通
<span class="color-blue">
{{ formData.vipChatUserCout == -1 ? '无限' : formData.vipChatUserCout }}
</span>
<span>人</span>
</div>
</el-text>
</el-form-item>
<el-form-item label-width="200px" label="VIP用户每天沟通次数" prop="pointTradeDeductUnitPrice">
<el-input-number v-model="formData.vipChatUserMsgCout" placeholder="" :min="-1" :max="100" />
<el-text class="w-full" size="small" type="info">
<div>
<span>VIP用户</span>
<span class="color-red">(会员)</span>
一天可以和其他用户发送
<span class="color-blue">
{{ formData.vipChatUserMsgCout == -1 ? '无限' : formData.vipChatUserMsgCout }}
</span>
<span>条消息</span>
</div>
</el-text>
</el-form-item>
<el-form-item label-width="200px" label="VIP用户每天解锁微信" prop="pointTradeDeductUnitPrice">
<el-input-number v-model="formData.vipLookWechtCout" placeholder="" :min="-1" :max="100" />
<el-text class="w-full" size="small" type="info">
<div>
<span>VIP用户</span>
<span class="color-red">(会员)</span>
一天可以解锁微信
<span class="color-blue">
{{ formData.vipLookWechtCout == -1 ? '无限' : formData.vipLookWechtCout }}
</span>
<span>次</span>
</div>
</el-text>
</el-form-item>
<!-- end VIP用户 end -->
</el-tab-pane>
<el-tab-pane v-if="false" label="积分">
<el-form-item label="积分抵扣" prop="pointTradeDeductEnable">
<el-switch v-model="formData.pointTradeDeductEnable" style="user-select: none" />
<el-text class="w-full" size="small" type="info">下单积分是否抵用订单金额</el-text>
</el-form-item>
<el-form-item label="积分抵扣" prop="pointTradeDeductUnitPrice">
<el-input-number v-model="computedPointTradeDeductUnitPrice" placeholder="请输入积分抵扣金额" :precision="2" />
<el-text class="w-full" size="small" type="info">
积分抵用比例(1 积分抵多少金额),单位:元
</el-text>
</el-form-item>
<el-form-item label="积分抵扣最大值" prop="pointTradeDeductMaxPrice">
<el-input-number v-model="formData.pointTradeDeductMaxPrice" placeholder="请输入积分抵扣最大值" />
<el-text class="w-full" size="small" type="info">
单次下单积分使用上限,0 不限制
</el-text>
</el-form-item>
<el-form-item label="1 元赠送多少分" prop="pointTradeGivePoint">
<el-input-number v-model="formData.pointTradeGivePoint" placeholder="请输入 1 元赠送多少积分" />
<el-text class="w-full" size="small" type="info">
下单支付金额按比例赠送积分(实际支付 1 元赠送多少积分)
</el-text>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="举报">
<el-form-item label-width="200px" label="举报类型" prop="reportTypeList">
<el-select style="width: 80%" v-model="formData.reportTypeList" allow-create filterable multiple
placeholder="请直接输入举报类型">
<el-option v-for="reason in formData.reportTypeList" :key="reason" :label="reason" :value="reason" />
</el-select>
<el-text class="w-full" size="small" type="info">
<div>
<span>用户</span>
<span class="color-red">举报</span>
<span>功能中的</span>
<span class="color-red">举报类型</span>
</div>
</el-text>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="礼物">
<el-form-item label-width="200px" label="礼物缘豆比例" prop="pointTradeDeductUnitPrice">
<el-input-number v-model="formData.giftCashRatio" placeholder="" :min="-1" :max="100" />
<el-text class="w-full" size="small" type="info">
<div>
<span>{{ formData.giftCashRatio }}</span>
<span class="color-blue">缘豆</span>
<sapn>可以兑换</sapn>
<span class="color-red">1</span>
<span>人民币</span>
</div>
</el-text>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="新人">
<el-form-item label-width="200px" label="新人判定" prop="pointTradeDeductUnitPrice">
<el-input-number v-model="formData.newPersonDays" placeholder="" :min="1" :max="365" />
<el-text class="w-full" size="small" type="info">
<div>
<span>入驻平台未满</span>
<span class="color-red">{{ formData.newPersonDays }}</span>
<span>天,都视为新人</span>
<span class="color-blue">新人</span>
</div>
</el-text>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="审核">
<el-form-item label-width="200px" label="修改微信是否自动通过审核" prop="pointTradeDeductUnitPrice">
<el-switch v-model="formData.autoWechatNumAuth"
style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949" active-text="自动通过审核"
inactive-text="需要人工审核" />
</el-form-item>
</el-tab-pane>
</el-tabs>
<el-form-item>
<el-button type="primary" @click="onSubmit">保存</el-button>
</el-form-item>
</el-form>
</ContentWrap>
</template>
<script lang="ts" setup>
import * as ConfigApi from '@/api/member/config'
defineOptions({ name: 'MemberConfig' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const formData = ref({
id: undefined,
defaultChatUserCout: 0,
defaultChatUserMsgCout: 0,
pointTradeDeductEnable: true,
pointTradeDeductUnitPrice: 0,
pointTradeDeductMaxPrice: 0,
vipChatUserCout: 0,
vipChatUserMsgCout: 0,
vipLookWechtCout: 0,
newPersonDays: 0,
giftCashRatio: 0,
pointTradeGivePoint: 0,
reportTypeList: [],
autoWechatNumAuth: false
})
// 创建一个计算属性,用于将 pointTradeDeductUnitPrice 显示为带两位小数的形式
const computedPointTradeDeductUnitPrice = computed({
get: () => (formData.value.pointTradeDeductUnitPrice / 100).toFixed(2),
set: (newValue: number) => {
formData.value.pointTradeDeductUnitPrice = Math.round(newValue * 100)
}
})
const formRules = reactive({})
const formRef = ref() // 表单 Ref
/** 修改积分配置 */
const onSubmit = async () => {
// 校验表单
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
// 提交请求
formLoading.value = true
try {
const data = formData.value as unknown as ConfigApi.ConfigVO
await ConfigApi.saveConfig(data)
message.success(t('common.updateSuccess'))
dialogVisible.value = false
} finally {
formLoading.value = false
}
}
/** 获得积分配置 */
const getConfig = async () => {
try {
const data = await ConfigApi.getConfig()
if (data === null) {
return
}
formData.value = data
} finally {
}
}
onMounted(() => {
getConfig()
})
</script>
<template>
<doc-alert title="会员手册(功能开启)" url="https://doc.iocoder.cn/member/build/" />
<ContentWrap>
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="120px" v-loading="formLoading">
<el-form-item label="hideId" v-show="false">
<el-input v-model="formData.id" />
</el-form-item>
<el-tabs>
<el-tab-pane label="聊天">
<!-- start 普通用户 start -->
<el-form-item label-width="200px" label="普通用户每天沟通人数" prop="pointTradeDeductUnitPrice">
<el-input-number v-model="formData.defaultChatUserCout" placeholder="" :min="-1" :max="100" />
<el-text class="w-full" size="small" type="info">
<div>
<span>普通用户</span>
<span class="color-red">(非会员)</span>
一天可以免费沟通
<span class="color-blue">
{{ formData.defaultChatUserCout == -1 ? '无限' : formData.defaultChatUserCout }}
</span>
<span>人</span>
</div>
</el-text>
</el-form-item>
<el-form-item label-width="200px" label="普通用户每天沟通次数" prop="pointTradeDeductUnitPrice">
<el-input-number v-model="formData.defaultChatUserMsgCout" placeholder="" :min="-1" :max="100" />
<el-text class="w-full" size="small" type="info">
<div>
<span>普通用户</span>
<span class="color-red">(非会员)</span>
一天可以免费和其他用户发送
<span class="color-blue">
{{ formData.defaultChatUserMsgCout == -1 ? '无限' : formData.defaultChatUserMsgCout }}
</span>
<span>条消息</span>
</div>
</el-text>
</el-form-item>
<!-- end 普通用户 end -->
<!-- start VIP用户 start -->
<el-form-item label-width="200px" label="VIP用户每天沟通人数" prop="pointTradeDeductUnitPrice">
<el-input-number v-model="formData.vipChatUserCout" placeholder="" :min="-1" :max="100" />
<el-text class="w-full" size="small" type="info">
<div>
<span>VIP用户</span>
<span class="color-red">(会员)</span>
一天可以沟通
<span class="color-blue">
{{ formData.vipChatUserCout == -1 ? '无限' : formData.vipChatUserCout }}
</span>
<span>人</span>
</div>
</el-text>
</el-form-item>
<el-form-item label-width="200px" label="VIP用户每天沟通次数" prop="pointTradeDeductUnitPrice">
<el-input-number v-model="formData.vipChatUserMsgCout" placeholder="" :min="-1" :max="100" />
<el-text class="w-full" size="small" type="info">
<div>
<span>VIP用户</span>
<span class="color-red">(会员)</span>
一天可以和其他用户发送
<span class="color-blue">
{{ formData.vipChatUserMsgCout == -1 ? '无限' : formData.vipChatUserMsgCout }}
</span>
<span>条消息</span>
</div>
</el-text>
</el-form-item>
<el-form-item label-width="200px" label="VIP用户每天解锁微信" prop="pointTradeDeductUnitPrice">
<el-input-number v-model="formData.vipLookWechtCout" placeholder="" :min="-1" :max="100" />
<el-text class="w-full" size="small" type="info">
<div>
<span>VIP用户</span>
<span class="color-red">(会员)</span>
一天可以解锁微信
<span class="color-blue">
{{ formData.vipLookWechtCout == -1 ? '无限' : formData.vipLookWechtCout }}
</span>
<span>次</span>
</div>
</el-text>
</el-form-item>
<!-- end VIP用户 end -->
</el-tab-pane>
<el-tab-pane v-if="false" label="积分">
<el-form-item label="积分抵扣" prop="pointTradeDeductEnable">
<el-switch v-model="formData.pointTradeDeductEnable" style="user-select: none" />
<el-text class="w-full" size="small" type="info">下单积分是否抵用订单金额</el-text>
</el-form-item>
<el-form-item label="积分抵扣" prop="pointTradeDeductUnitPrice">
<el-input-number v-model="computedPointTradeDeductUnitPrice" placeholder="请输入积分抵扣金额" :precision="2" />
<el-text class="w-full" size="small" type="info">
积分抵用比例(1 积分抵多少金额),单位:元
</el-text>
</el-form-item>
<el-form-item label="积分抵扣最大值" prop="pointTradeDeductMaxPrice">
<el-input-number v-model="formData.pointTradeDeductMaxPrice" placeholder="请输入积分抵扣最大值" />
<el-text class="w-full" size="small" type="info">
单次下单积分使用上限,0 不限制
</el-text>
</el-form-item>
<el-form-item label="1 元赠送多少分" prop="pointTradeGivePoint">
<el-input-number v-model="formData.pointTradeGivePoint" placeholder="请输入 1 元赠送多少积分" />
<el-text class="w-full" size="small" type="info">
下单支付金额按比例赠送积分(实际支付 1 元赠送多少积分)
</el-text>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="举报">
<el-form-item label-width="200px" label="举报类型" prop="reportTypeList">
<el-select style="width: 80%" v-model="formData.reportTypeList" allow-create filterable multiple
placeholder="请直接输入举报类型">
<el-option v-for="reason in formData.reportTypeList" :key="reason" :label="reason" :value="reason" />
</el-select>
<el-text class="w-full" size="small" type="info">
<div>
<span>用户</span>
<span class="color-red">举报</span>
<span>功能中的</span>
<span class="color-red">举报类型</span>
</div>
</el-text>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="新人">
<el-form-item label-width="200px" label="新人判定" prop="pointTradeDeductUnitPrice">
<el-input-number v-model="formData.newPersonDays" placeholder="" :min="1" :max="365" />
<el-text class="w-full" size="small" type="info">
<div>
<span>入驻平台未满</span>
<span class="color-red">{{ formData.newPersonDays }}</span>
<span>天,都视为新人</span>
<span class="color-blue">新人</span>
</div>
</el-text>
</el-form-item>
</el-tab-pane>
</el-tabs>
<el-form-item>
<el-button type="primary" @click="onSubmit">保存</el-button>
</el-form-item>
</el-form>
</ContentWrap>
</template>
<script lang="ts" setup>
import * as ConfigApi from '@/api/member/config'
defineOptions({ name: 'MemberConfig' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const formData = ref({
id: undefined,
defaultChatUserCout: 0,
defaultChatUserMsgCout: 0,
pointTradeDeductEnable: true,
pointTradeDeductUnitPrice: 0,
pointTradeDeductMaxPrice: 0,
vipChatUserCout: 0,
vipChatUserMsgCout: 0,
vipLookWechtCout: 0,
newPersonDays: 0,
giftCashRatio: 0,
pointTradeGivePoint: 0,
reportTypeList: [],
autoWechatNumAuth: false
})
// 创建一个计算属性,用于将 pointTradeDeductUnitPrice 显示为带两位小数的形式
const computedPointTradeDeductUnitPrice = computed({
get: () => (formData.value.pointTradeDeductUnitPrice / 100).toFixed(2),
set: (newValue: number) => {
formData.value.pointTradeDeductUnitPrice = Math.round(newValue * 100)
}
})
const formRules = reactive({})
const formRef = ref() // 表单 Ref
/** 修改积分配置 */
const onSubmit = async () => {
// 校验表单
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
// 提交请求
formLoading.value = true
try {
const data = formData.value as unknown as ConfigApi.ConfigVO
await ConfigApi.saveConfig(data)
message.success(t('common.updateSuccess'))
dialogVisible.value = false
} finally {
formLoading.value = false
}
}
/** 获得积分配置 */
const getConfig = async () => {
try {
const data = await ConfigApi.getConfig()
if (data === null) {
return
}
formData.value = data
} finally {
}
}
onMounted(() => {
getConfig()
})
</script>
+27 -13
View File
@@ -54,16 +54,14 @@ v-model="queryParams.title" placeholder="请输入会员标题" clearable @keyup
<el-table-column label="支付相关" width="300px" align="center">
<template #default="{ row }">
<div class="flex flex-col items-start">
<div class="flex">
<div class="flex items-center">
<span class="w-80px text-left font-size-12px">支付状态:</span>
<div>
<el-tag size="small" v-if="!row.payStatus" type="danger">未支付</el-tag>
<el-tag size="small" v-else type="primary">未支付</el-tag>
</div>
<el-tag size="small" v-if="isPaid(row)" type="success">支付成功</el-tag>
<el-tag size="small" v-else type="danger">未支付</el-tag>
</div>
<div class="table-cell">
<span>支付时间:</span>
<span v-if="row.payTime">{{ row.payTime }}</span>
<span v-if="row.payTime">{{ formatDate(row.payTime) }}</span>
<span v-else class="color-red!">未支付</span>
</div>
</div>
@@ -72,11 +70,13 @@ v-model="queryParams.title" placeholder="请输入会员标题" clearable @keyup
<el-table-column label="退款相关" width="200px" align="center">
<template #default="{ row }">
<div class="flex flex-col items-start">
<div class="table-cell">
<span>退款状态:</span>
<span v-if="row.refundStatus">
{{ row.refundStatus }}
</span>
<div class="flex items-center">
<span class="w-80px text-left font-size-12px">退款状态:</span>
<dict-tag
v-if="row.refundStatus != null"
:type="DICT_TYPE.PAY_REFUND_STATUS"
:value="row.refundStatus"
/>
<span v-else class="color-red!">未退款</span>
</div>
<div class="table-cell">
@@ -86,7 +86,7 @@ v-model="queryParams.title" placeholder="请输入会员标题" clearable @keyup
</div>
<div class="table-cell">
<span>退款时间:</span>
<span v-if="row.refundTime">{{ row.refundTime }}</span>
<span v-if="row.refundTime">{{ formatDate(row.refundTime) }}</span>
<span v-else class="color-red!">未退款</span>
</div>
</div>
@@ -94,7 +94,12 @@ v-model="queryParams.title" placeholder="请输入会员标题" clearable @keyup
</el-table-column>
<el-table-column label="操作" align="center" min-width="120px">
<template #default="{ row }">
<el-button v-if="row.payStatus && !row.refundStatus" link type="primary" @click="handleRefund(row)">
<el-button
v-if="isPaid(row) && !row.payRefundId && row.refundStatus !== 10"
link
type="primary"
@click="handleRefund(row)"
>
退款
</el-button>
</template>
@@ -109,6 +114,8 @@ v-model="queryParams.title" placeholder="请输入会员标题" clearable @keyup
<script setup lang="ts">
import { fenToYuan } from "@/utils/index"
import { formatDate } from '@/utils/formatTime'
import { DICT_TYPE } from '@/utils/dict'
import * as OpenTypeHistoryApi from '@/api/member/opentypehistory'
const message = useMessage()
@@ -126,6 +133,13 @@ const queryParams = reactive({
createTime: []
})
/** bit(1) 映射异常时,以支付时间为准 */
const isPaid = (row: any) => {
if (row?.payTime) return true
const status = row?.payStatus
return status === true || status === 1 || status === '1'
}
// 退款
const handleRefund = async (row: any) => {
try {
+23 -3
View File
@@ -46,7 +46,7 @@ v-model="queryParams.createTime" value-format="YYYY-MM-DD HH:mm:ss" type="datera
</el-table-column>
<el-table-column label="举报类型" align="center" width="150">
<template #default="{ row }">
<el-tag type="danger">{{ row.reportType }}</el-tag>
<el-tag type="danger">{{ formatReportType(row.reportType) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="举报图片" align="center" width="150">
@@ -62,7 +62,7 @@ v-model="queryParams.createTime" value-format="YYYY-MM-DD HH:mm:ss" type="datera
<span>在</span>
<span class="color-red">{{ formatPast(row.createTime) }}</span>
<span>以</span>
<span class="color-red">{{ row.reportType }}</span>
<span class="color-red">{{ formatReportType(row.reportType) }}</span>
<span>理由,举报了我(</span>
<span class="color-blue">{{ row.memberNickname }}</span>
<span>)的动态</span>
@@ -84,6 +84,7 @@ import { FormInstance } from 'element-plus'
import { dateFormatter, formatPast } from '@/utils/formatTime'
import * as operationApi from "@/api/member/operation"
import * as ConfigApi from '@/api/member/config'
const props = defineProps({
memberId: {
@@ -98,6 +99,17 @@ const queryFormRef = ref<FormInstance>()
const total = ref(0)
const loading = ref(true)
const list = ref([])
const reportTypeList = ref<string[]>([])
/** App 存的是 reportTypeList 下标,需映射成文案 */
const formatReportType = (reportType: string | number | null | undefined) => {
if (reportType === null || reportType === undefined || reportType === '') return '-'
const idx = Number(reportType)
if (!Number.isNaN(idx) && idx >= 0 && idx < reportTypeList.value.length) {
return reportTypeList.value[idx]
}
return String(reportType)
}
// 表单搜索
const queryParams = ref({
@@ -141,7 +153,15 @@ const getList = async () => {
}
}
onMounted(() => {
const getConfig = async () => {
try {
const data = await ConfigApi.getConfig()
reportTypeList.value = data?.reportTypeList || []
} catch {}
}
onMounted(async () => {
await getConfig()
getList()
})
</script>
+52 -8
View File
@@ -58,9 +58,9 @@ v-model="queryParams.memberNickname" placeholder="请输入被举报用户" clea
</div>
</template>
</el-table-column>
<el-table-column label="举报类型" align="center" width="100">
<el-table-column label="举报类型" align="center" width="160">
<template #default="{ row }">
<el-tag type="danger">{{ row.reportType }}</el-tag>
<el-tag type="danger">{{ formatReportType(row.reportType) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="举报图片" align="center" min-width="150">
@@ -70,6 +70,19 @@ v-model="queryParams.memberNickname" placeholder="请输入被举报用户" clea
</el-table-column>
<el-table-column label="举报内容" align="center" prop="reportDescription" min-width="150" />
<el-table-column label="举报时间" align="center" prop="createTime" :formatter="dateFormatter" width="180" />
<el-table-column label="操作" align="center" fixed="right" width="120">
<template #default="{ row }">
<el-button
link
type="danger"
v-hasPermi="['member:user:update']"
:disabled="!row.memberId"
@click="handleBanReportedUser(row)"
>
封禁用户
</el-button>
</template>
</el-table-column>
</el-table>
<Pagination
v-model:limit="queryParams.pageSize" v-model:page="queryParams.pageNo" :total="total"
@@ -79,10 +92,12 @@ v-model:limit="queryParams.pageSize" v-model:page="queryParams.pageNo" :total="t
<script setup lang="ts">
import { dateFormatter } from '@/utils/formatTime'
import { OperationVO, OperationApi } from '@/api/member/operation'
import { OperationApi } from '@/api/member/operation'
import * as ConfigApi from '@/api/member/config'
import * as UserApi from '@/api/member/user'
import { CommonStatusEnum } from '@/utils/constants'
const message = useMessage()
const queryFormRef = ref()
const loading = ref(true)
@@ -96,10 +111,22 @@ const queryParams = reactive({
memberNickname: '',
createTime: []
})
const memberConfig = reactive({
const memberConfig = reactive<{ reportTypeList?: string[] }>({
reportTypeList: []
})
/** App 存的是 reportTypeList 下标,需映射成文案 */
const formatReportType = (reportType: string | number | null | undefined) => {
if (reportType === null || reportType === undefined || reportType === '') return '-'
const list = memberConfig.reportTypeList || []
const idx = Number(reportType)
if (!Number.isNaN(idx) && idx >= 0 && idx < list.length) {
return list[idx]
}
// 历史数据可能直接存了文案
return String(reportType)
}
/** 查询列表 */
const getList = async () => {
loading.value = true
@@ -132,9 +159,26 @@ const handleQuery = () => {
getList()
}
onMounted(() => {
/** 封禁被举报用户 */
const handleBanReportedUser = async (row: any) => {
if (!row.memberId) {
message.warning('未找到被举报用户')
return
}
try {
const nickname = row.memberNickname || row.memberId
await message.confirm(`确认封禁被举报用户「${nickname}」?封禁后将无法登录 App。`)
await UserApi.updateUserStatus({
id: row.memberId,
status: CommonStatusEnum.DISABLE
})
message.success('封禁成功')
} catch {}
}
onMounted(async () => {
await getConfig()
getList()
getConfig()
})
</script>
+9
View File
@@ -0,0 +1,9 @@
<template>
<UserList :sex="2" />
</template>
<script lang="ts" setup>
// 女号管理:会员管理按性别(女)过滤
import UserList from './index.vue'
defineOptions({ name: 'MemberUserFemale' })
</script>
+362 -361
View File
@@ -1,369 +1,370 @@
<template>
<doc-alert title="会员用户、标签、分组" url="https://doc.iocoder.cn/member/user/" />
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form ref="queryFormRef" :inline="true" :model="queryParams" class="-mb-15px" label-width="68px">
<el-form-item label="用户昵称" prop="nickname">
<template>
<doc-alert title="会员用户、标签、分组" url="https://doc.iocoder.cn/member/user/" />
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form ref="queryFormRef" :inline="true" :model="queryParams" class="-mb-15px" label-width="68px">
<el-form-item label="用户昵称" prop="nickname">
<el-input
v-model="queryParams.nickname" class="!w-240px" clearable placeholder="请输入用户昵称"
@keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="手机号" prop="mobile">
v-model="queryParams.nickname"
class="!w-240px"
clearable
placeholder="请输入用户昵称"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="用户ID" prop="userCode">
<el-input
v-model="queryParams.mobile" class="!w-240px" clearable placeholder="请输入手机号"
@keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="注册时间" prop="createTime">
v-model="queryParams.userCode"
class="!w-240px"
clearable
placeholder="App 我的页面显示的 ID"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="手机号" prop="mobile">
<el-input
v-model="queryParams.mobile"
class="!w-240px"
clearable
placeholder="请输入手机号"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="注册时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" class="!w-240px" end-placeholder="结束日期"
start-placeholder="开始日期" type="daterange" value-format="YYYY-MM-DD HH:mm:ss" />
</el-form-item>
<el-form-item label="登录时间" prop="loginDate">
v-model="queryParams.createTime"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px"
end-placeholder="结束日期"
start-placeholder="开始日期"
type="daterange"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
<el-form-item label="登录时间" prop="loginDate">
<el-date-picker
v-model="queryParams.loginDate" :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px" end-placeholder="结束日期" start-placeholder="开始日期" type="daterange"
value-format="YYYY-MM-DD HH:mm:ss" />
</el-form-item>
<el-form-item v-if="false" label="用户标签" prop="tagIds">
<MemberTagSelect v-model="queryParams.tagIds" />
</el-form-item>
<el-form-item v-if="false" label="用户等级" prop="levelId">
<MemberLevelSelect v-model="queryParams.levelId" />
</el-form-item>
<el-form-item v-if="false" label="用户分组" prop="groupId">
<MemberGroupSelect v-model="queryParams.groupId" />
</el-form-item>
<el-form-item>
<el-button @click="handleQuery">
<Icon class="mr-5px" icon="ep:search" />
搜索
</el-button>
<el-button @click="resetQuery">
<Icon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
<el-button v-hasPermi="['promotion:coupon:send']" @click="openCoupon">发送优惠券</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
v-model="queryParams.loginDate"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px"
end-placeholder="结束日期"
start-placeholder="开始日期"
type="daterange"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
<el-form-item v-if="false" label="用户标签" prop="tagIds">
<MemberTagSelect v-model="queryParams.tagIds" />
</el-form-item>
<el-form-item v-if="false" label="用户等级" prop="levelId">
<MemberLevelSelect v-model="queryParams.levelId" />
</el-form-item>
<el-form-item v-if="false" label="用户分组" prop="groupId">
<MemberGroupSelect v-model="queryParams.groupId" />
</el-form-item>
<el-form-item>
<el-button @click="handleQuery">
<Icon class="mr-5px" icon="ep:search" />
搜索
</el-button>
<el-button @click="resetQuery">
<Icon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
<el-button v-hasPermi="['promotion:coupon:send']" @click="openCoupon">发送优惠券</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true"
@selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" />
<el-table-column align="center" label="用户编号" prop="id" width="120px" />
<el-table-column align="center" label="头像" prop="avatar" width="80px">
<template #default="{ row }">
<div class="w-[50px] h-[50px]">
<PreviewImage :src="row.avatar" />
</div>
</template>
</el-table-column>
<el-table-column align="center" label="手机号" prop="mobile" width="120px" />
<el-table-column align="center" label="昵称" prop="nickname" width="120px" />
<el-table-column align="center" label="会员" prop="nickname" />
<el-table-column align="center" width="160px" label="用户标签">
<template #default="{ row }">
<div class="grid grid-cols-2 gap-1">
v-loading="loading"
:data="list"
:show-overflow-tooltip="true"
:stripe="true"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column align="center" label="用户编号" prop="id" width="120px" />
<el-table-column align="center" label="用户ID" prop="userCode" width="100px" />
<el-table-column align="center" label="性别" prop="sex" width="70px">
<template #default="{ row }">
<dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="row.sex" />
</template>
</el-table-column>
<el-table-column align="center" label="头像" prop="avatar" width="80px">
<template #default="{ row }">
<div class="w-[50px] h-[50px]">
<PreviewImage :src="row.avatar" />
</div>
</template>
</el-table-column>
<el-table-column align="center" label="手机号" prop="mobile" width="120px" />
<el-table-column align="center" label="昵称" prop="nickname" width="120px" />
<el-table-column align="center" label="会员" prop="nickname" />
<el-table-column align="center" width="160px" label="用户标签">
<template #default="{ row }">
<div class="grid grid-cols-2 gap-1">
<dict-tag
v-for="v in row.memberTagList" :key="v.tagBizType" :type="DICT_TYPE.MEMBER_RECENT_TAG"
:value="v.tagBizType" />
</div>
</template>
</el-table-column>
<el-table-column :show-overflow-tooltip="false" align="center" width="250px" label="用户配置">
<template #default="{ row }">
<div class="grid grid-cols-2 gap-1">
<div class="switch-cell">
<span>是否推送:</span>
<span>
<el-switch
v-model="row.recentCfg.isPush" size="small" :active-value="true" :inactive-value="false"
@change="memberCfgChange(row)" />
</span>
</div>
<div class="switch-cell">
<span>首页隐身:</span>
<span>
<el-switch
v-model="row.recentCfg.homeHide" size="small" :active-value="true" :inactive-value="false"
@change="memberCfgChange(row)" />
</span>
</div>
<div class="switch-cell">
<span>隐私保护</span>
<span>
<el-switch
v-model="row.recentCfg.privacySecure" size="small" :active-value="true"
:inactive-value="false" @change="memberCfgChange(row)" />
</span>
</div>
<div class="switch-cell">
<span>短信通知</span>
<span>
<el-switch
v-model="row.recentCfg.shortNote" size="small" :active-value="true" :inactive-value="false"
@change="memberCfgChange(row)" />
</span>
</div>
</div>
</template>
</el-table-column>
<el-table-column align="center" label="状态" prop="status" width="100px">
<template #default="{ row }">
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="row.status" />
</template>
</el-table-column>
<el-table-column align="center" label="时间" width="250px">
<template #default="{ row }">
<div>
<div>
<span>登录时间:</span>
<span>{{ formatDate(row.loginDate) }}</span>
</div>
<div>
<span>注册时间:</span>
<span>{{ formatDate(row.createTime) }}</span>
</div>
</div>
</template>
</el-table-column>
<el-table-column :show-overflow-tooltip="false" align="center" fixed="right" label="操作" width="100px">
<template #default="{ row }">
<div class="flex items-center justify-center">
<el-button link type="primary" @click="openDetail(row.id)">详情</el-button>
v-for="v in row.memberTagList"
:key="v.tagBizType"
:type="DICT_TYPE.MEMBER_RECENT_TAG"
:value="v.tagBizType"
/>
</div>
</template>
</el-table-column>
<el-table-column align="center" label="是否禁用" width="100px">
<template #default="{ row }">
<div class="switch-cell">
<el-switch
v-model="row.status"
size="small"
:active-value="CommonStatusEnum.DISABLE"
:inactive-value="CommonStatusEnum.ENABLE"
:disabled="!checkPermi(['member:user:update'])"
@change="handleStatusChange(row)"
/>
</div>
</template>
</el-table-column>
<el-table-column align="center" label="时间" width="250px">
<template #default="{ row }">
<div>
<div>
<span>登录时间:</span>
<span>{{ formatDate(row.loginDate) }}</span>
</div>
<div>
<span>注册时间:</span>
<span>{{ formatDate(row.createTime) }}</span>
</div>
</div>
</template>
</el-table-column>
<el-table-column :show-overflow-tooltip="false" align="center" fixed="right" label="操作" width="100px">
<template #default="{ row }">
<div class="flex items-center justify-center">
<el-button link type="primary" @click="openDetail(row.id)">详情</el-button>
<el-dropdown
v-hasPermi="[
'member:user:update',
'member:user:update-level',
'member:user:update-point',
'pay:wallet:update-balance'
]" @command="(command) => handleCommand(command, row)">
<el-button link type="primary">
<Icon icon="ep:d-arrow-right" />
更多
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-if="checkPermi(['member:user:update'])" command="handleUpdate">
编辑
</el-dropdown-item>
<el-dropdown-item v-if="checkPermi(['member:user:update-level'])" command="handleUpdateLevel">
修改等级
</el-dropdown-item>
<el-dropdown-item v-if="checkPermi(['member:user:update-point'])" command="handleUpdatePoint">
修改积分
</el-dropdown-item>
<el-dropdown-item v-if="checkPermi(['pay:wallet:update-balance'])" command="handleUpdateBlance">
修改宝石
</el-dropdown-item>
<el-dropdown-item v-if="checkPermi(['pay:wallet:update-member-tag'])" command="handleUpdateMemberTag">
修改用户标签
</el-dropdown-item>
<el-dropdown-item v-if="checkPermi(['pay:wallet:update-member-tag'])" command="handleOpenVipTag">
开通会员
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
v-hasPermi="[
'member:user:update',
'member:user:update-level',
'member:user:update-point'
]"
@command="(command) => handleCommand(command, row)"
>
<el-button link type="primary">
<Icon icon="ep:d-arrow-right" />
更多
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-if="checkPermi(['member:user:update'])" command="handleUpdate">
编辑
</el-dropdown-item>
<el-dropdown-item
v-if="checkPermi(['member:user:update-level'])"
command="handleUpdateLevel"
>
修改等级
</el-dropdown-item>
<el-dropdown-item
v-if="checkPermi(['member:user:update-point'])"
command="handleUpdatePoint"
>
修改积分
</el-dropdown-item>
<el-dropdown-item
v-if="checkPermi(['pay:wallet:update-member-tag'])"
command="handleUpdateMemberTag"
>
修改用户标签
</el-dropdown-item>
<el-dropdown-item
v-if="checkPermi(['pay:wallet:update-member-tag'])"
command="handleOpenVipTag"
>
开通会员
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
v-model:limit="queryParams.pageSize" v-model:page="queryParams.pageNo" :total="total"
@pagination="getList" />
</ContentWrap>
<!-- 表单弹窗:添加/修改 -->
<UserForm ref="formRef" @success="getList" />
<!-- 修改用户等级弹窗 -->
<UserLevelUpdateForm ref="updateLevelFormRef" @success="getList" />
<!-- 修改用户积分弹窗 -->
<UserPointUpdateForm ref="updatePointFormRef" @success="getList" />
<!-- 修改用户余额弹窗 -->
<UserBalanceUpdateForm ref="UpdateBalanceFormRef" @success="getList" />
<!-- 发送优惠券弹窗 -->
<CouponSendForm ref="couponSendFormRef" />
<!-- 修改用户标签弹窗 -->
<UserMemberTagForm ref="updateMemberTagFormRef" @success="getList" />
<!-- 开通会员弹窗 -->
<UserOpenVipForm ref="userOpenVipFormRef" @success="getList" />
<!-- 修改用户设置 -->
<UserSettingForm ref="userSettingFormRef" @success="getList" />
</template>
<script lang="ts" setup>
import { formatDate } from '@/utils/formatTime'
import * as UserApi from '@/api/member/user'
import { DICT_TYPE } from '@/utils/dict'
import UserForm from './UserForm.vue'
import MemberTagSelect from '@/views/member/tag/components/MemberTagSelect.vue'
import MemberLevelSelect from '@/views/member/level/components/MemberLevelSelect.vue'
import MemberGroupSelect from '@/views/member/group/components/MemberGroupSelect.vue'
import UserLevelUpdateForm from './components/UserLevelUpdateForm.vue'
import UserPointUpdateForm from './components/UserPointUpdateForm.vue'
import UserBalanceUpdateForm from './components/UserBalanceUpdateForm.vue'
import UserMemberTagForm from "./components/UserMemberTagForm.vue"
import UserOpenVipForm from "./components/UserOpenVipForm.vue"
import { CouponSendForm } from '@/views/mall/promotion/coupon/components'
import { checkPermi } from '@/utils/permission'
import * as RecentCfgApi from '@/api/member/recent-cfg'
defineOptions({ name: 'MemberUser' })
const message = useMessage() // 消息弹窗
const loading = ref(true) // 列表的加载中
const total = ref(0) // 列表的总页数
const list = ref([]) // 列表的数据
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
nickname: null,
mobile: null,
loginDate: [],
createTime: [],
tagIds: [],
levelId: null,
groupId: null
})
const queryFormRef = ref() // 搜索的表单
const updateLevelFormRef = ref() // 修改会员等级表单
const updatePointFormRef = ref() // 修改会员积分表单
const UpdateBalanceFormRef = ref() // 修改用户余额表单
const updateMemberTagFormRef = ref() // 修改用户标签表单
const userSettingFormRef = ref() // 修改用户设置表单
const userOpenVipFormRef = ref() // 开通会员
const selectedIds = ref<number[]>([]) // 表格的选中 ID 数组
// 修改朋友圈配置
const memberCfgChange = async (row: any) => {
loading.value = true
try {
const _params = {
...row.recentCfg,
memberId: row.id
}
await RecentCfgApi.getRecentPage(_params)
message.success("操作成功")
getList()
} catch (error) {
console.log("|修改朋友圈配置|,意外的错误", error);
} finally {
loading.value = false
}
}
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await UserApi.getUserPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 打开会员详情 */
const { push } = useRouter()
const openDetail = (id: number) => {
push({ name: 'MemberUserDetail', params: { id } })
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 表格选中事件 */
const handleSelectionChange = (rows: UserApi.UserVO[]) => {
selectedIds.value = rows.map((row) => row.id)
}
/** 发送优惠券 */
const couponSendFormRef = ref()
const openCoupon = () => {
if (selectedIds.value.length === 0) {
message.warning('请选择要发送优惠券的用户')
return
}
couponSendFormRef.value.open(selectedIds.value)
}
/** 操作分发 */
const handleCommand = (command: string, row: UserApi.UserVO) => {
switch (command) {
case 'handleUpdate':
openForm('update', row.id)
break
case 'handleUpdateLevel':
updateLevelFormRef.value.open(row.id)
break
case 'handleUpdatePoint':
updatePointFormRef.value.open(row.id)
break
case 'handleUpdateBlance':
UpdateBalanceFormRef.value.open(row.id)
break
case 'handleUpdateMemberTag':
updateMemberTagFormRef.value.open(row.id)
break
case 'handleOpenVipTag':
userOpenVipFormRef.value.open(row.id)
break
case 'handleUpdateMemberCfg':
userSettingFormRef.value.open(row.id)
break
default:
break
}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>
<style lang="scss" scoped>
.switch-cell {
display: flex;
justify-content: flex-start;
align-items: center;
span {
display: inline-block;
text-align: left;
font-size: 12px;
}
span:first-child {
width: 70px;
}
span:last-child {
flex: 1;
}
}
</style>
v-model:limit="queryParams.pageSize"
v-model:page="queryParams.pageNo"
:total="total"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗:添加/修改 -->
<UserForm ref="formRef" @success="getList" />
<!-- 修改用户等级弹窗 -->
<UserLevelUpdateForm ref="updateLevelFormRef" @success="getList" />
<!-- 修改用户积分弹窗 -->
<UserPointUpdateForm ref="updatePointFormRef" @success="getList" />
<!-- 发送优惠券弹窗 -->
<CouponSendForm ref="couponSendFormRef" />
<!-- 修改用户标签弹窗 -->
<UserMemberTagForm ref="updateMemberTagFormRef" @success="getList" />
<!-- 开通会员弹窗 -->
<UserOpenVipForm ref="userOpenVipFormRef" @success="getList" />
</template>
<script lang="ts" setup>
import { formatDate } from '@/utils/formatTime'
import * as UserApi from '@/api/member/user'
import { DICT_TYPE } from '@/utils/dict'
import { CommonStatusEnum } from '@/utils/constants'
import UserForm from './UserForm.vue'
import MemberTagSelect from '@/views/member/tag/components/MemberTagSelect.vue'
import MemberLevelSelect from '@/views/member/level/components/MemberLevelSelect.vue'
import MemberGroupSelect from '@/views/member/group/components/MemberGroupSelect.vue'
import UserLevelUpdateForm from './components/UserLevelUpdateForm.vue'
import UserPointUpdateForm from './components/UserPointUpdateForm.vue'
import UserMemberTagForm from './components/UserMemberTagForm.vue'
import UserOpenVipForm from './components/UserOpenVipForm.vue'
import { CouponSendForm } from '@/views/mall/promotion/coupon/components'
import { checkPermi } from '@/utils/permission'
defineOptions({ name: 'MemberUser' })
const props = defineProps<{ sex?: number }>()
const message = useMessage() // 消息弹窗
const loading = ref(true) // 列表的加载中
const total = ref(0) // 列表的总页数
const list = ref([]) // 列表的数据
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
nickname: null,
mobile: null,
userCode: null,
sex: props.sex ?? null,
loginDate: [],
createTime: [],
tagIds: [],
levelId: null,
groupId: null
})
const queryFormRef = ref() // 搜索的表单
const updateLevelFormRef = ref() // 修改会员等级表单
const updatePointFormRef = ref() // 修改会员积分表单
const updateMemberTagFormRef = ref() // 修改用户标签表单
const userOpenVipFormRef = ref() // 开通会员
const selectedIds = ref<number[]>([]) // 表格的选中 ID 数组
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await UserApi.getUserPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 打开会员详情 */
const { push } = useRouter()
const openDetail = (id: number) => {
push({ name: 'MemberUserDetail', params: { id } })
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 表格选中事件 */
const handleSelectionChange = (rows: UserApi.UserVO[]) => {
selectedIds.value = rows.map((row) => row.id)
}
/** 发送优惠券 */
const couponSendFormRef = ref()
const openCoupon = () => {
if (selectedIds.value.length === 0) {
message.warning('请选择要发送优惠券的用户')
return
}
couponSendFormRef.value.open(selectedIds.value)
}
/** 是否禁用开关(参考原用户配置里的是否推送) */
const handleStatusChange = async (row: UserApi.UserVO) => {
const isDisable = row.status === CommonStatusEnum.DISABLE
const nickname = row.nickname || row.mobile || row.id
try {
await message.confirm(
isDisable
? `确认禁用用户「${nickname}」?禁用后将无法登录 App。`
: `确认启用用户「${nickname}」?`
)
await UserApi.updateUserStatus({ id: row.id, status: row.status })
message.success(isDisable ? '禁用成功' : '启用成功')
} catch {
// 取消或失败时回滚开关
row.status =
row.status === CommonStatusEnum.ENABLE ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE
}
}
/** 操作分发 */
const handleCommand = (command: string, row: UserApi.UserVO) => {
switch (command) {
case 'handleUpdate':
openForm('update', row.id)
break
case 'handleUpdateLevel':
updateLevelFormRef.value.open(row.id)
break
case 'handleUpdatePoint':
updatePointFormRef.value.open(row.id)
break
case 'handleUpdateMemberTag':
updateMemberTagFormRef.value.open(row.id)
break
case 'handleOpenVipTag':
userOpenVipFormRef.value.open(row.id)
break
default:
break
}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>
<style lang="scss" scoped>
.switch-cell {
display: flex;
justify-content: center;
align-items: center;
}
</style>
+9
View File
@@ -0,0 +1,9 @@
<template>
<UserList :sex="1" />
</template>
<script lang="ts" setup>
// 男号管理:会员管理按性别(男)过滤
import UserList from './index.vue'
defineOptions({ name: 'MemberUserMale' })
</script>
+273
View File
@@ -0,0 +1,273 @@
<template>
<ContentWrap>
<!-- 操作工具栏 -->
<div class="mb-10px">
<el-button
v-hasPermi="['system:sub-account:manage']"
plain
type="primary"
@click="openForm()"
>
<Icon class="mr-5px" icon="ep:plus" />
添加子账号
</el-button>
</div>
<!-- 列表 -->
<el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
<el-table-column align="center" label="登录账号" prop="username" width="160px" />
<el-table-column align="center" label="昵称" prop="nickname" width="160px" />
<el-table-column align="center" label="状态" prop="status" width="100px">
<template #default="{ row }">
<el-switch
v-model="row.status"
:active-value="0"
:inactive-value="1"
:disabled="!checkPermi(['system:sub-account:manage'])"
@change="handleStatusChange(row)"
/>
</template>
</el-table-column>
<el-table-column align="center" label="可见菜单数" width="100px">
<template #default="{ row }">
{{ (row.menuIds || []).length }}
</template>
</el-table-column>
<el-table-column align="center" label="最后登录时间" prop="loginDate" width="180px">
<template #default="{ row }">
<span>{{ row.loginDate ? formatDate(row.loginDate) : '从未登录' }}</span>
</template>
</el-table-column>
<el-table-column align="center" label="创建时间" prop="createTime" width="180px">
<template #default="{ row }">
<span>{{ formatDate(row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column align="center" label="操作" width="240px">
<template #default="{ row }">
<el-button link type="primary" @click="openForm(row)">编辑</el-button>
<el-button link type="primary" @click="openResetPassword(row)">重置密码</el-button>
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</ContentWrap>
<!-- 添加/修改子账号弹窗 -->
<Dialog v-model="formDialog.visible" :title="formDialog.title" width="640px">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="90px">
<el-form-item label="登录账号" prop="username">
<el-input
v-model="formData.username"
:disabled="formDialog.editId !== undefined"
placeholder="字母 + 数字,4-30 位"
/>
</el-form-item>
<el-form-item label="昵称" prop="nickname">
<el-input v-model="formData.nickname" placeholder="请输入子账号昵称" />
</el-form-item>
<el-form-item v-if="formDialog.editId === undefined" label="登录密码" prop="password">
<el-input v-model="formData.password" placeholder="4-16 位" show-password />
</el-form-item>
<el-form-item label="可见菜单" prop="menuIds">
<div class="w-full">
<el-tree
ref="menuTreeRef"
:data="menuTree"
:props="{ label: 'name', children: 'children' }"
default-expand-all
empty-text="当前账号未拥有任何菜单"
node-key="id"
show-checkbox
@check="handleMenuCheck"
/>
<div class="mt-5px text-12px text-gray-400">
子账号登录后台后,只能看到这里勾选的菜单(范围不超过当前账号自己的菜单)
</div>
</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="formDialog.visible = false">取 消</el-button>
<el-button :loading="formDialog.submitting" type="primary" @click="submitForm">确 定</el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import { formatDate } from '@/utils/formatTime'
import { handleTree } from '@/utils/tree'
import { checkPermi } from '@/utils/permission'
import { ElMessageBox } from 'element-plus'
import * as SubAccountApi from '@/api/system/subaccount'
defineOptions({ name: 'SystemSubAccount' })
const message = useMessage() // 消息弹窗
const loading = ref(true) // 列表的加载中
const list = ref<SubAccountApi.SubAccountVO[]>([]) // 子账号列表
const menuTree = ref<any[]>([]) // 当前账号拥有的菜单树
const menuTreeRef = ref() // 菜单树组件
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
list.value = await SubAccountApi.getSubAccountList()
} finally {
loading.value = false
}
}
/** 加载当前账号拥有的菜单树 */
const loadMenuTree = async () => {
const menus = await SubAccountApi.getHolderMenuList()
menuTree.value = handleTree(menus, 'id', 'parentId')
}
/** 表单弹窗 */
const formDialog = reactive({
visible: false,
title: '',
editId: undefined as number | undefined,
submitting: false
})
const formRef = ref()
const formData = reactive({
username: '',
nickname: '',
password: '',
menuIds: [] as number[]
})
const formRules = {
username: [
{ required: true, message: '登录账号不能为空', trigger: 'blur' },
{ min: 4, max: 30, message: '长度为 4-30 个字符', trigger: 'blur' }
],
nickname: [{ required: true, message: '昵称不能为空', trigger: 'blur' }],
password: [
{ required: true, message: '登录密码不能为空', trigger: 'blur' },
{ min: 4, max: 16, message: '密码长度为 4-16 位', trigger: 'blur' }
]
}
/** 打开添加/编辑弹窗 */
const openForm = async (row?: SubAccountApi.SubAccountVO) => {
formDialog.editId = row?.userId
formDialog.title = row ? '编辑子账号' : '添加子账号'
formData.username = row?.username || ''
formData.nickname = row?.nickname || ''
formData.password = ''
formData.menuIds = []
if (!menuTree.value.length) {
await loadMenuTree()
}
formDialog.visible = true
await nextTick()
// 先清空上次弹窗残留的勾选
menuTreeRef.value?.setCheckedKeys([])
if (row?.menuIds?.length) {
// 只回显叶子节点:父子联动模式下,把父节点传给 setCheckedKeys
// 会把它下面所有子节点全部勾上,导致整个目录被选满
const leafIds = new Set(collectLeafIds(menuTree.value))
menuTreeRef.value?.setCheckedKeys(
row.menuIds.filter((id) => leafIds.has(id))
)
handleMenuCheck()
}
}
/** 收集树中所有叶子节点 id */
const collectLeafIds = (nodes: any[]): number[] => {
const ids: number[] = []
const walk = (list: any[]) => {
list.forEach((node) => {
if (node.children?.length) {
walk(node.children)
} else {
ids.push(node.id)
}
})
}
walk(nodes)
return ids
}
/** 勾选变化时,同步半选的父节点(保证父目录可见) */
const handleMenuCheck = () => {
const checked = menuTreeRef.value?.getCheckedKeys() || []
const halfChecked = menuTreeRef.value?.getHalfCheckedKeys() || []
formData.menuIds = [...checked, ...halfChecked]
}
/** 提交表单 */
const submitForm = async () => {
await formRef.value.validate()
handleMenuCheck()
formDialog.submitting = true
try {
if (formDialog.editId !== undefined) {
await SubAccountApi.updateSubAccount(formDialog.editId, {
...formData
} as SubAccountApi.SubAccountVO)
message.success('修改成功')
} else {
await SubAccountApi.createSubAccount({
...formData
} as SubAccountApi.SubAccountVO & { password: string })
message.success('创建成功')
}
formDialog.visible = false
await getList()
} finally {
formDialog.submitting = false
}
}
/** 修改状态开关 */
const handleStatusChange = async (row: SubAccountApi.SubAccountVO) => {
const disable = row.status === 1
try {
await message.confirm(
disable
? `确认禁用子账号「${row.username}」?禁用后将无法登录后台。`
: `确认启用子账号「${row.username}」?`
)
await SubAccountApi.updateSubAccountStatus(row.userId, row.status)
message.success(disable ? '禁用成功' : '启用成功')
} catch {
row.status = disable ? 0 : 1 // 取消时回滚
}
}
/** 重置密码 */
const openResetPassword = async (row: SubAccountApi.SubAccountVO) => {
const { value } = await ElMessageBox.prompt(
`请输入子账号「${row.username}」的新密码(4-16 位)`,
'重置密码',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPattern: /^.{4,16}$/,
inputErrorMessage: '密码长度为 4-16 位',
inputType: 'password'
}
)
await SubAccountApi.resetSubAccountPassword(row.userId, value)
message.success('重置成功')
}
/** 删除 */
const handleDelete = async (row: SubAccountApi.SubAccountVO) => {
await message.confirm(`确认删除子账号「${row.username}」?删除后无法恢复。`)
await SubAccountApi.deleteSubAccount(row.userId)
message.success('删除成功')
await getList()
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>