Compare commits
43 Commits
130a253b83
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 64fdaba93d | |||
| 50d5f54015 | |||
| a77592dec6 | |||
| 00867ff960 | |||
| 4c08bd1f8c | |||
| ab6f5c787e | |||
| 79a7ea8c56 | |||
| b5ba0ca3fb | |||
| 9b5e2807b5 | |||
| 3a30e81183 | |||
| 28386f5429 | |||
| b0b8689acb | |||
| 8012cb82fc | |||
| 73b2da2d1e | |||
| 6f8e5036f7 | |||
| daab8ec41a | |||
| b513d701d8 | |||
| f8eaa554b3 | |||
| 9cee70bc62 | |||
| ffd917815d | |||
| 3841fcc7e5 | |||
| ac7bfeacfe | |||
| d9a20c906b | |||
| 5300a1ec2e | |||
| 68820dfcea | |||
| d8ee7b3263 | |||
| 3f47005f76 | |||
| f0f0c88f18 | |||
| 0f611ad926 | |||
| ba71b504e7 | |||
| 117a7f408f | |||
| 05c3d518fa | |||
| ea24625805 | |||
| 845829d088 | |||
| 530de8b31c | |||
| 3cddae93aa | |||
| 1cd851ec8e | |||
| d2aa4c1207 | |||
| a7e31613c5 | |||
| 98b073e8f5 | |||
| d85333d058 | |||
| 9d615c8500 | |||
| 3b68bbe1c9 |
+4
-1
@@ -1,2 +1,5 @@
|
|||||||
# 生产环境
|
# 生产环境
|
||||||
VITE_API_BASE_URL = https://api.cpms.example.com/admin
|
VITE_API_BASE_URL = https://cpmsapi.yunvip123.com
|
||||||
|
|
||||||
|
# 是否开启本地调试
|
||||||
|
LOCAL_DEBUG=false
|
||||||
|
|||||||
Generated
+1721
File diff suppressed because it is too large
Load Diff
@@ -6,10 +6,15 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vite",
|
"serve": "vite",
|
||||||
|
"serve:prod": "vite --mode production",
|
||||||
"build:dev": "vue-tsc --noEmit && vite build --mode development",
|
"build:dev": "vue-tsc --noEmit && vite build --mode development",
|
||||||
"build:test": "vue-tsc --noEmit && vite build --mode test",
|
"build:test": "vue-tsc --noEmit && vite build --mode test",
|
||||||
"build:prod": "vue-tsc --noEmit && vite build --mode production",
|
"build:prod": "vue-tsc --noEmit && vite build --mode production",
|
||||||
|
"test": "vitest",
|
||||||
|
"test:run": "vitest run",
|
||||||
|
"test:coverage": "vitest run --coverage",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
|
"preview:prod": "pnpm build:prod && vite preview",
|
||||||
"type-check": "vue-tsc --noEmit",
|
"type-check": "vue-tsc --noEmit",
|
||||||
"lint": "eslint src --ext .js,.ts,.tsx",
|
"lint": "eslint src --ext .js,.ts,.tsx",
|
||||||
"lint:fix": "eslint src --ext .js,.ts,.tsx --fix",
|
"lint:fix": "eslint src --ext .js,.ts,.tsx --fix",
|
||||||
@@ -42,17 +47,21 @@
|
|||||||
"@typescript-eslint/parser": "^6.0.0",
|
"@typescript-eslint/parser": "^6.0.0",
|
||||||
"@vitejs/plugin-vue": "^4.5.0",
|
"@vitejs/plugin-vue": "^4.5.0",
|
||||||
"@vitejs/plugin-vue-jsx": "^4.0.0",
|
"@vitejs/plugin-vue-jsx": "^4.0.0",
|
||||||
|
"@vitest/coverage-v8": "2.1.9",
|
||||||
|
"@vue/test-utils": "^2.4.11",
|
||||||
"eslint": "^8.0.0",
|
"eslint": "^8.0.0",
|
||||||
"eslint-config-prettier": "^9.0.0",
|
"eslint-config-prettier": "^9.0.0",
|
||||||
"eslint-plugin-prettier": "^5.0.0",
|
"eslint-plugin-prettier": "^5.0.0",
|
||||||
"eslint-plugin-vue": "^9.0.0",
|
"eslint-plugin-vue": "^9.0.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
|
"jsdom": "^19.0.0",
|
||||||
"less": "^4.6.4",
|
"less": "^4.6.4",
|
||||||
"lint-staged": "^15.0.0",
|
"lint-staged": "^15.0.0",
|
||||||
"prettier": "^3.0.0",
|
"prettier": "^3.0.0",
|
||||||
"typescript": "^5.3.0",
|
"typescript": "^5.3.0",
|
||||||
"vite": "^5.0.0",
|
"vite": "^5.0.0",
|
||||||
"vite-plugin-remove-console": "^2.2.0",
|
"vite-plugin-remove-console": "^2.2.0",
|
||||||
|
"vitest": "^2.1.9",
|
||||||
"vue-tsc": "^2.0.0"
|
"vue-tsc": "^2.0.0"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# ============================================================
|
||||||
|
# CPMS 运营平台 - 权限标识字符串列表
|
||||||
|
# 生成规则:
|
||||||
|
# 1. 每个页面都有一个 "页面名-查看" 的默认权限
|
||||||
|
# 2. 表格操作列中的按钮生成对应操作权限
|
||||||
|
# 3. 同一关键字的两个状态按钮(如上架/下架、启用/禁用)合并为一个权限
|
||||||
|
# 4. 筛选栏中有"新增"按钮的页面,生成新增权限
|
||||||
|
# 5. 标识为英文,附带中文说明(含按钮所在位置)
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
# ─── 赛事管理模块 ─────────────────────────────────────────
|
||||||
|
|
||||||
|
events.list.view # 赛事列表-查看(页面默认查看权限)
|
||||||
|
events.list.toggle_status # 赛事列表-上架/下架(表格操作列,根据赛事状态显示"上架"或"下架"按钮,合并为一个权限)
|
||||||
|
events.regulation.view # 赛事规程-查看(赛事列表表格中的"赛事规程"列,点击"查看"链接)
|
||||||
|
|
||||||
|
events.orders.view # 订单管理-查看(页面默认查看权限)
|
||||||
|
events.orders.refund # 订单管理-重新退款(表格操作列,退款状态为3时显示"重新退款"按钮)
|
||||||
|
|
||||||
|
events.users.view # 用户列表-查看(页面默认查看权限)
|
||||||
|
events.users.toggle_status # 用户列表-启用/禁用(表格操作列,根据用户状态显示"启用"或"禁用"按钮,合并为一个权限)
|
||||||
|
|
||||||
|
events.banner.view # Banner配置-查看(页面默认查看权限)
|
||||||
|
events.banner.add # Banner配置-新增(筛选栏中的"新增"按钮)
|
||||||
|
events.banner.edit # Banner配置-编辑(表格操作列中的"编辑"按钮)
|
||||||
|
events.banner.toggle_status # Banner配置-启用/禁用(表格操作列,根据状态显示"启用"或"禁用"按钮,合并为一个权限)
|
||||||
|
events.banner.delete # Banner配置-删除(表格操作列中的"删除"按钮,红色危险样式)
|
||||||
|
|
||||||
|
events.logs.view # 赛事操作日志-查看(页面默认查看权限,无操作列按钮)
|
||||||
|
|
||||||
|
# ─── 账务管理模块 ─────────────────────────────────────────
|
||||||
|
|
||||||
|
finance.withdraw.view # 提现申请-查看(页面默认查看权限)
|
||||||
|
finance.withdraw.audit # 提现申请-审核(表格操作列,审核状态为"待审核"时显示"审核"按钮)
|
||||||
|
|
||||||
|
finance.wallet.view # 用户钱包-查看(页面默认查看权限)
|
||||||
|
finance.wallet.detail # 用户钱包-查看明细(表格操作列中的"明细"按钮,打开交易明细弹窗)
|
||||||
|
|
||||||
|
finance.payments.view # 支付流水-查看(页面默认查看权限,无操作列按钮,仅展示数据+金额汇总)
|
||||||
|
|
||||||
|
finance.reports.view # 账务报表-查看(页面默认查看权限,无操作列按钮,展示统计卡+趋势图+明细表)
|
||||||
|
|
||||||
|
# ─── 系统管理模块 ─────────────────────────────────────────
|
||||||
|
|
||||||
|
system.users.view # 用户管理-查看(页面默认查看权限)
|
||||||
|
system.users.add # 用户管理-新增(筛选栏中的"新增"按钮)
|
||||||
|
system.users.edit # 用户管理-编辑(表格操作列中的"编辑"按钮)
|
||||||
|
system.users.toggle_status # 用户管理-启用/禁用(表格操作列,根据用户状态显示"启用"或"禁用"按钮,合并为一个权限)
|
||||||
|
|
||||||
|
system.roles.view # 角色权限-查看(页面默认查看权限)
|
||||||
|
system.roles.add # 角色权限-新增(筛选栏中的"新增角色"按钮)
|
||||||
|
system.roles.edit # 角色权限-编辑(表格操作列中的"编辑"按钮)
|
||||||
|
system.roles.delete # 角色权限-删除(表格操作列中的"删除"按钮,红色危险样式)
|
||||||
|
|
||||||
|
system.logs.view # 操作日志-查看(页面默认查看权限,无操作列按钮)
|
||||||
|
|
||||||
|
system.params.view # 参数配置-查看
|
||||||
Generated
+1045
-1
File diff suppressed because it is too large
Load Diff
@@ -47,7 +47,7 @@ export interface BannerActiveParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiResult<T> {
|
export interface ApiResult<T> {
|
||||||
code: number;
|
code: number | string;
|
||||||
msg: string;
|
msg: string;
|
||||||
data: T;
|
data: T;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export interface PageData<T> {
|
|||||||
/** 统一响应包裹 */
|
/** 统一响应包裹 */
|
||||||
export interface ApiResult<T> {
|
export interface ApiResult<T> {
|
||||||
/** 编码:200表示成功,其他值表示失败 */
|
/** 编码:200表示成功,其他值表示失败 */
|
||||||
code: number;
|
code: number | string;
|
||||||
/** 消息内容 */
|
/** 消息内容 */
|
||||||
msg: string;
|
msg: string;
|
||||||
/** 响应数据 */
|
/** 响应数据 */
|
||||||
|
|||||||
@@ -5,8 +5,15 @@ export type { LoginResult } from './types';
|
|||||||
|
|
||||||
export const login = (params: { phone: string; password: string }) => {
|
export const login = (params: { phone: string; password: string }) => {
|
||||||
return post(`/op/login`, params) as Promise<{
|
return post(`/op/login`, params) as Promise<{
|
||||||
code: number;
|
code: number | string;
|
||||||
msg: string;
|
msg: string;
|
||||||
data: LoginResult;
|
data: LoginResult;
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const logout = () => {
|
||||||
|
return post(`/op/logout`) as Promise<{
|
||||||
|
code: number | string;
|
||||||
|
msg: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export interface PageData<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiResult<T> {
|
export interface ApiResult<T> {
|
||||||
code: number;
|
code: number | string;
|
||||||
msg: string;
|
msg: string;
|
||||||
data: T;
|
data: T;
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-5
@@ -18,6 +18,7 @@ export interface AuthData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const AUTH_FETCH_FAIL_MSG = '账户权限获取失败,请重新登录';
|
const AUTH_FETCH_FAIL_MSG = '账户权限获取失败,请重新登录';
|
||||||
|
const AUTH_EMPTY_MSG = '当前账户权限无任何权限,请联系管理员';
|
||||||
|
|
||||||
function normalizeAuthData(res: any): AuthData {
|
function normalizeAuthData(res: any): AuthData {
|
||||||
if (res?.code != null && res.code != 200) {
|
if (res?.code != null && res.code != 200) {
|
||||||
@@ -29,7 +30,7 @@ function normalizeAuthData(res: any): AuthData {
|
|||||||
const permsList = data.permsList || [];
|
const permsList = data.permsList || [];
|
||||||
|
|
||||||
if (permsList.length === 0) {
|
if (permsList.length === 0) {
|
||||||
throw new Error(AUTH_FETCH_FAIL_MSG);
|
throw new Error(AUTH_EMPTY_MSG);
|
||||||
}
|
}
|
||||||
|
|
||||||
const nickname = data.nickName || '';
|
const nickname = data.nickName || '';
|
||||||
@@ -46,9 +47,9 @@ function normalizeAuthData(res: any): AuthData {
|
|||||||
return { roleList, permsList, nickname, phone };
|
return { roleList, permsList, nickname, phone };
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleAuthFetchFailure() {
|
function handleAuthFetchFailure(msg?: string) {
|
||||||
clearAuthDataCache();
|
clearAuthDataCache();
|
||||||
forceReLogin(AUTH_FETCH_FAIL_MSG);
|
forceReLogin(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -62,8 +63,8 @@ export function fetchAuthData(): Promise<AuthData> {
|
|||||||
|
|
||||||
const promise = get('/op/permission')
|
const promise = get('/op/permission')
|
||||||
.then((res) => normalizeAuthData(res))
|
.then((res) => normalizeAuthData(res))
|
||||||
.catch((err) => {
|
.catch((err: Error) => {
|
||||||
handleAuthFetchFailure();
|
handleAuthFetchFailure(err.message);
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+9
-11
@@ -88,7 +88,7 @@ export interface PageData<T> {
|
|||||||
|
|
||||||
/** 统一响应包裹 */
|
/** 统一响应包裹 */
|
||||||
export interface ApiResult<T> {
|
export interface ApiResult<T> {
|
||||||
code: number;
|
code: number | string;
|
||||||
msg: string;
|
msg: string;
|
||||||
data: T;
|
data: T;
|
||||||
}
|
}
|
||||||
@@ -99,14 +99,8 @@ export interface ApiResult<T> {
|
|||||||
|
|
||||||
/** 退款记录 */
|
/** 退款记录 */
|
||||||
export interface InnerRefundInfo {
|
export interface InnerRefundInfo {
|
||||||
/** 已退金额;0.00格式 */
|
|
||||||
refundedAmount: string;
|
|
||||||
/** 总退款次数(接口实际返回 string) */
|
|
||||||
totalRefundCount: string;
|
|
||||||
/** 退款金额;0.00格式 */
|
/** 退款金额;0.00格式 */
|
||||||
refundAmount: string;
|
refundAmount: string;
|
||||||
/** 是否全部退款;0=否,1=是(接口实际返回 string) */
|
|
||||||
isFullRefund: string;
|
|
||||||
/** 退款方式 */
|
/** 退款方式 */
|
||||||
refundMethod: string;
|
refundMethod: string;
|
||||||
/** 退款申请时间 */
|
/** 退款申请时间 */
|
||||||
@@ -115,6 +109,10 @@ export interface InnerRefundInfo {
|
|||||||
refundOutTradeNo: string;
|
refundOutTradeNo: string;
|
||||||
/** 退款完成时间 */
|
/** 退款完成时间 */
|
||||||
refundTime: string;
|
refundTime: string;
|
||||||
|
/** 退款状态;1=退款中,2=退款成功,3=退款失败 */
|
||||||
|
refundStatus: string;
|
||||||
|
/** 退款选手名称合集文字 */
|
||||||
|
refundSignupNames: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 订单详情 */
|
/** 订单详情 */
|
||||||
@@ -142,9 +140,9 @@ export interface TournamentAdminOrderInfoVO {
|
|||||||
/** 订单详情查询参数 */
|
/** 订单详情查询参数 */
|
||||||
export interface OrderDetailQueryParams {
|
export interface OrderDetailQueryParams {
|
||||||
/** 订单编号 */
|
/** 订单编号 */
|
||||||
orderNo: string;
|
orderNo?: string;
|
||||||
/** 选手ID */
|
/** 选手ID */
|
||||||
uniqueId: string;
|
uniqueId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -180,9 +178,9 @@ export interface TournamentAdminOrderInfoPageVO {
|
|||||||
/** 选手列表查询参数 */
|
/** 选手列表查询参数 */
|
||||||
export interface OrderPlayerListQueryParams {
|
export interface OrderPlayerListQueryParams {
|
||||||
/** 订单编号 */
|
/** 订单编号 */
|
||||||
orderNo: string;
|
orderNo?: string;
|
||||||
/** 选手ID */
|
/** 选手ID */
|
||||||
uniqueId: string;
|
uniqueId?: string;
|
||||||
page: string;
|
page: string;
|
||||||
limit: string;
|
limit: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* 参数配置 API
|
||||||
|
*
|
||||||
|
* TODO: 等后端接口确定后,替换占位的 URL 和返回类型
|
||||||
|
*/
|
||||||
|
import { get, post, put, del } from '@/utils/request';
|
||||||
|
import type { ParamQueryParams, SysParamVO, PageData, ApiResult } from './types';
|
||||||
|
|
||||||
|
// 类型重导出,方便页面 model 层统一引用
|
||||||
|
export type { SysParamVO, ParamQueryParams, PageData, ApiResult } from './types';
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// URL 常量(占位,等后端接口确定后替换)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/** 配置列表分页查询 */
|
||||||
|
const PARAM_LIST = '/admin/sys/param/page';
|
||||||
|
/** 新增配置 */
|
||||||
|
const PARAM_ADD = '/admin/sys/param/add';
|
||||||
|
/** 编辑配置 */
|
||||||
|
const PARAM_UPDATE = '/admin/sys/param/update';
|
||||||
|
/** 删除配置 */
|
||||||
|
const PARAM_DELETE = '/admin/sys/param/delete';
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// API 函数(占位)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/** 配置列表分页 */
|
||||||
|
export async function getParamList(
|
||||||
|
params: ParamQueryParams,
|
||||||
|
): Promise<ApiResult<PageData<SysParamVO>>> {
|
||||||
|
return get(PARAM_LIST, params as Record<string, any>);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 新增配置 */
|
||||||
|
export async function addParam(data: Partial<SysParamVO>): Promise<ApiResult<null>> {
|
||||||
|
return post(PARAM_ADD, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 编辑配置 */
|
||||||
|
export async function updateParam(data: Partial<SysParamVO>): Promise<ApiResult<null>> {
|
||||||
|
return put(PARAM_UPDATE, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除配置 */
|
||||||
|
export async function deleteParam(id: number): Promise<ApiResult<null>> {
|
||||||
|
return del(`${PARAM_DELETE}`, { id });
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* 参数配置 — 类型定义
|
||||||
|
*
|
||||||
|
* TODO: 等后端接口确定后,根据实际字段补充完整类型
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** 配置项列表项 */
|
||||||
|
export interface SysParamVO {
|
||||||
|
/** 配置项 ID */
|
||||||
|
id: number;
|
||||||
|
/** 配置项 key */
|
||||||
|
// paramKey: string;
|
||||||
|
/** 配置项 value */
|
||||||
|
// paramValue: string;
|
||||||
|
/** 备注 */
|
||||||
|
// remark: string;
|
||||||
|
/** 更新时间 */
|
||||||
|
// updateTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 配置列表查询参数 */
|
||||||
|
export interface ParamQueryParams {
|
||||||
|
page?: string;
|
||||||
|
limit?: string;
|
||||||
|
/** 搜索关键字 */
|
||||||
|
// keyword?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 分页数据结构 */
|
||||||
|
export interface PageData<T> {
|
||||||
|
total: number;
|
||||||
|
list: T[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** API 统一响应格式 */
|
||||||
|
export interface ApiResult<T> {
|
||||||
|
code: number | string;
|
||||||
|
msg: string;
|
||||||
|
data: T;
|
||||||
|
}
|
||||||
@@ -58,7 +58,7 @@ export interface PaymentFlowPageData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiResult<T> {
|
export interface ApiResult<T> {
|
||||||
code: number;
|
code: number | string;
|
||||||
msg: string;
|
msg: string;
|
||||||
data: T;
|
data: T;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export interface ReportQueryParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiResult<T> {
|
export interface ApiResult<T> {
|
||||||
code: number;
|
code: number | string;
|
||||||
msg: string;
|
msg: string;
|
||||||
data: T;
|
data: T;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export interface PageData<T> {
|
|||||||
list: T[];
|
list: T[];
|
||||||
}
|
}
|
||||||
export interface ApiResult<T> {
|
export interface ApiResult<T> {
|
||||||
code: number;
|
code: number | string;
|
||||||
msg: string;
|
msg: string;
|
||||||
data: T;
|
data: T;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export interface PageData<T> {
|
|||||||
list: T[];
|
list: T[];
|
||||||
}
|
}
|
||||||
export interface ApiResult<T> {
|
export interface ApiResult<T> {
|
||||||
code: number;
|
code: number | string;
|
||||||
msg: string;
|
msg: string;
|
||||||
data: T;
|
data: T;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export interface PageData<T> {
|
|||||||
list: T[];
|
list: T[];
|
||||||
}
|
}
|
||||||
export interface ApiResult<T> {
|
export interface ApiResult<T> {
|
||||||
code: number;
|
code: number | string;
|
||||||
msg: string;
|
msg: string;
|
||||||
data: T;
|
data: T;
|
||||||
}
|
}
|
||||||
@@ -84,6 +84,8 @@ export interface WalletTransactionVO {
|
|||||||
orderNo: string;
|
orderNo: string;
|
||||||
/** 时间 */
|
/** 时间 */
|
||||||
createDate: string;
|
createDate: string;
|
||||||
|
/** 状态;1=已解冻,2=冻结中,3=正常 */
|
||||||
|
status: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 交易明细查询参数 */
|
/** 交易明细查询参数 */
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export interface WithdrawVO {
|
|||||||
auditStatus: number;
|
auditStatus: number;
|
||||||
/** 到账状态:0=未支付,1=支付成功,2=支付失败 */
|
/** 到账状态:0=未支付,1=支付成功,2=支付失败 */
|
||||||
payStatus: number;
|
payStatus: number;
|
||||||
/** 流水单号 */
|
/** 第三方单号 */
|
||||||
serialNumber: string;
|
serialNumber: string;
|
||||||
/** 申请时间 */
|
/** 申请时间 */
|
||||||
createDate: string;
|
createDate: string;
|
||||||
@@ -50,7 +50,7 @@ export interface PageData<T> {
|
|||||||
list: T[];
|
list: T[];
|
||||||
}
|
}
|
||||||
export interface ApiResult<T> {
|
export interface ApiResult<T> {
|
||||||
code: number;
|
code: number | string;
|
||||||
msg: string;
|
msg: string;
|
||||||
data: T;
|
data: T;
|
||||||
}
|
}
|
||||||
@@ -101,4 +101,6 @@ export interface WithdrawInfoVO {
|
|||||||
auditImg: string;
|
auditImg: string;
|
||||||
/** 审核图片列表 */
|
/** 审核图片列表 */
|
||||||
auditImgList: string[];
|
auditImgList: string[];
|
||||||
|
/** 审核结果 */
|
||||||
|
checkMsg: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 8.7 KiB |
@@ -165,9 +165,9 @@ const PageTabs = defineComponent({
|
|||||||
case 'other':
|
case 'other':
|
||||||
props.onCloseOther?.(props.activeKey);
|
props.onCloseOther?.(props.activeKey);
|
||||||
break;
|
break;
|
||||||
case 'all':
|
// case 'all':
|
||||||
props.onCloseAll?.();
|
// props.onCloseAll?.();
|
||||||
break;
|
// break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -278,7 +278,7 @@ const PageTabs = defineComponent({
|
|||||||
{renderBatchMenuItem('left', renderAiIcon(LuPanelLeftClose), '关闭左侧标签页')}
|
{renderBatchMenuItem('left', renderAiIcon(LuPanelLeftClose), '关闭左侧标签页')}
|
||||||
{renderBatchMenuItem('right', renderAiIcon(LuPanelRightClose), '关闭右侧标签页')}
|
{renderBatchMenuItem('right', renderAiIcon(LuPanelRightClose), '关闭右侧标签页')}
|
||||||
{renderBatchMenuItem('other', renderAiIcon(LuCopyX), '关闭其它标签页')}
|
{renderBatchMenuItem('other', renderAiIcon(LuCopyX), '关闭其它标签页')}
|
||||||
{renderBatchMenuItem('all', renderAiIcon(LuListX), '关闭全部标签页')}
|
{/* {renderBatchMenuItem('all', renderAiIcon(LuListX), '关闭全部标签页')} */}
|
||||||
</Menu>
|
</Menu>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -164,6 +164,15 @@ export const FALLBACK_MENU_NODES: MenuNode[] = [
|
|||||||
icon: 'AiOutlineSafetyCertificate',
|
icon: 'AiOutlineSafetyCertificate',
|
||||||
permission: 'system.roles.view',
|
permission: 'system.roles.view',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'system_params',
|
||||||
|
name: '参数配置',
|
||||||
|
path: 'system/params',
|
||||||
|
component: 'system/params',
|
||||||
|
componentName: 'SystemParams',
|
||||||
|
icon: 'AiOutlineSetting',
|
||||||
|
permission: 'system.params.view',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'system_logs',
|
id: 'system_logs',
|
||||||
name: '操作日志',
|
name: '操作日志',
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { usePermissionStore } from '@/stores/permissionStore';
|
|||||||
import { useTabsStore } from '@/stores/tabsStore';
|
import { useTabsStore } from '@/stores/tabsStore';
|
||||||
import { useUserStore } from '@/stores/userStore';
|
import { useUserStore } from '@/stores/userStore';
|
||||||
import { clearAuthDataCache } from '@/api/authCache';
|
import { clearAuthDataCache } from '@/api/authCache';
|
||||||
|
import { logout as logoutApi } from '@/api/login';
|
||||||
import router from '@/router';
|
import router from '@/router';
|
||||||
|
|
||||||
const TOKEN_KEY = 'MY_APP_AUTH_TOKEN';
|
const TOKEN_KEY = 'MY_APP_AUTH_TOKEN';
|
||||||
@@ -66,9 +67,14 @@ function createAuth() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 退出:清除 token → 清除菜单+权限 → 清除动态路由 → 跳转登录页
|
* 退出:调用登出接口 → 清除 token → 清除菜单+权限 → 清除动态路由 → 跳转登录页
|
||||||
*/
|
*/
|
||||||
const logout = () => {
|
const logout = async () => {
|
||||||
|
try {
|
||||||
|
await logoutApi();
|
||||||
|
} catch {
|
||||||
|
// 即使接口失败也要继续清除本地会话
|
||||||
|
}
|
||||||
clearSession();
|
clearSession();
|
||||||
router.push('/login');
|
router.push('/login');
|
||||||
};
|
};
|
||||||
@@ -95,7 +101,7 @@ export function forceReLogin(msg = '账户权限获取失败,请重新登录')
|
|||||||
notification.error({
|
notification.error({
|
||||||
message: '提示',
|
message: '提示',
|
||||||
description: msg,
|
description: msg,
|
||||||
duration: 2600,
|
duration: 3,
|
||||||
});
|
});
|
||||||
auth.clearSession();
|
auth.clearSession();
|
||||||
router.push('/login');
|
router.push('/login');
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
const { menuItems } = useMenuStore();
|
const { menuItems } = useMenuStore();
|
||||||
const { phone, nickname } = useUserStore();
|
const { phone, nickname } = useUserStore();
|
||||||
const displayName = computed(() => nickname.value || phone.value);
|
const displayName = computed(() => phone.value || nickname.value);
|
||||||
const {
|
const {
|
||||||
tabs,
|
tabs,
|
||||||
cachedViews,
|
cachedViews,
|
||||||
@@ -108,13 +108,11 @@ export default defineComponent({
|
|||||||
/** ===== 退出登录 ===== */
|
/** ===== 退出登录 ===== */
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: '提示',
|
title: '确认退出',
|
||||||
content: '是否退出登录?',
|
content: '确认要退出当前账号吗?',
|
||||||
okText: '确认',
|
okText: '确认',
|
||||||
cancelText: '取消',
|
cancelText: '取消',
|
||||||
onOk: () => {
|
onOk: () => {
|
||||||
const { clearUser } = useUserStore();
|
|
||||||
clearUser();
|
|
||||||
auth.logout();
|
auth.logout();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -298,7 +296,7 @@ export default defineComponent({
|
|||||||
</div>
|
</div>
|
||||||
</Content>
|
</Content>
|
||||||
|
|
||||||
<Footer style={footerStyle}>六个羽友赛事运营平台 ©{new Date().getFullYear()}</Footer>
|
{/* <Footer style={footerStyle}>六个羽友赛事运营平台 ©{new Date().getFullYear()}</Footer> */}
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
{/* 修改密码弹窗 */}
|
{/* 修改密码弹窗 */}
|
||||||
|
|||||||
@@ -132,7 +132,10 @@ export function useBannerModel() {
|
|||||||
// ===== 方法 =====
|
// ===== 方法 =====
|
||||||
|
|
||||||
/** 查询 */
|
/** 查询 */
|
||||||
const handleSearch = () => fetchList();
|
const handleSearch = () => {
|
||||||
|
pagination.reset();
|
||||||
|
fetchList();
|
||||||
|
};
|
||||||
|
|
||||||
/** 重置 */
|
/** 重置 */
|
||||||
const handleReset = useThrottleFn(async () => {
|
const handleReset = useThrottleFn(async () => {
|
||||||
|
|||||||
@@ -28,12 +28,29 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.desc {
|
.desc {
|
||||||
|
:global(.ant-descriptions-view table) {
|
||||||
|
table-layout: fixed;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
:global(.ant-descriptions-item-label) {
|
:global(.ant-descriptions-item-label) {
|
||||||
width: 110px;
|
width: 110px;
|
||||||
color: rgba(0, 0, 0, 0.65);
|
color: rgba(0, 0, 0, 0.65);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 首列内容区加宽(省市区、公开活动、创建时间等)
|
||||||
|
.wideCol:global(.ant-descriptions-item-content) {
|
||||||
|
width: 38%;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 较长 label 加宽且不换行(class 同时挂在 label/content 上,需用复合选择器)
|
||||||
|
.wideLabel:global(.ant-descriptions-item-label) {
|
||||||
|
width: 132px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
// Item 的 class 会同时挂在 label/content 两个同级 td/th 上,不能用后代选择器
|
// Item 的 class 会同时挂在 label/content 两个同级 td/th 上,不能用后代选择器
|
||||||
.statCell:global(.ant-descriptions-item-content) {
|
.statCell:global(.ant-descriptions-item-content) {
|
||||||
min-width: 60px;
|
min-width: 60px;
|
||||||
@@ -59,11 +76,6 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tableWrap {
|
|
||||||
max-height: 200px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.paginationRow {
|
.paginationRow {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
@@ -71,39 +83,37 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.divisionList {
|
.divisionList {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coverWrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
justify-content: center;
|
||||||
gap: 6px;
|
align-items: center;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
:global(.ant-image) {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.divisionItem {
|
.divisionItem {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: rgba(0, 0, 0, 0.65);
|
color: rgba(0, 0, 0, 0.65);
|
||||||
}
|
padding: 4px 12px;
|
||||||
|
background: #f5f5f5;
|
||||||
.idImageGrid {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.idImageCell {
|
|
||||||
width: 128px;
|
|
||||||
height: 82px;
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
overflow: hidden;
|
transition: all 0.2s ease;
|
||||||
border: 1px solid #f0f0f0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
:global(.ant-image) {
|
&:hover {
|
||||||
width: 100%;
|
color: #1677ff;
|
||||||
height: 100%;
|
background: #e6f4ff;
|
||||||
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { defineComponent, reactive, ref } from 'vue';
|
import { defineComponent, computed, reactive, ref } from 'vue';
|
||||||
import { Modal, Descriptions, Image, Table, Pagination, Spin } from 'ant-design-vue';
|
import { Modal, Descriptions, Image, Table, Pagination, Spin, Space } from 'ant-design-vue';
|
||||||
import { message } from 'ant-design-vue';
|
import { message } from 'ant-design-vue';
|
||||||
|
import { EyeOutlined } from '@ant-design/icons-vue';
|
||||||
import {
|
import {
|
||||||
getEventDetail,
|
getEventDetail,
|
||||||
getSignupList,
|
getSignupList,
|
||||||
@@ -9,6 +10,7 @@ import {
|
|||||||
type TournamentAdminInfoSignupVO,
|
type TournamentAdminInfoSignupVO,
|
||||||
} from '../model/services';
|
} from '../model/services';
|
||||||
import { useState, useEffect } from '@/hooks';
|
import { useState, useEffect } from '@/hooks';
|
||||||
|
import defaultCover from '@/assets/img/default.png';
|
||||||
import styles from './EventDetailModal.module.less';
|
import styles from './EventDetailModal.module.less';
|
||||||
|
|
||||||
interface EventDetailModalProps {
|
interface EventDetailModalProps {
|
||||||
@@ -27,27 +29,30 @@ const formatYN = (v: string) => YES_NO_MAP[v] ?? v;
|
|||||||
const formatPublic = (v: string) => PUBLIC_MAP[v] ?? v;
|
const formatPublic = (v: string) => PUBLIC_MAP[v] ?? v;
|
||||||
const formatGender = (v: string) => GENDER_MAP[v] ?? v;
|
const formatGender = (v: string) => GENDER_MAP[v] ?? v;
|
||||||
|
|
||||||
const PAGE_SIZE = 10;
|
const PAGE_SIZE = 5;
|
||||||
|
|
||||||
/** 渲染身份证图片(cover 填充,与封面显示方式一致) */
|
/** 渲染身份证图片(缩略图,点击可预览) */
|
||||||
const renderIdImages = (imgList: string[], singleImg: string) => {
|
const renderIdImages = (imgList: string[], singleImg: string) => {
|
||||||
const urls = imgList.length > 0 ? imgList : singleImg ? [singleImg] : [];
|
const urls = imgList.length > 0 ? imgList : singleImg ? [singleImg] : [];
|
||||||
if (urls.length === 0) return <span>--</span>;
|
if (urls.length === 0) return <span>--</span>;
|
||||||
return (
|
return (
|
||||||
<Image.PreviewGroup>
|
<Space size={4}>
|
||||||
<div class={styles.idImageGrid}>
|
{urls.map((url) => (
|
||||||
{urls.map((url) => (
|
<Image
|
||||||
<div class={styles.idImageCell}>
|
key={url}
|
||||||
<Image src={url} />
|
src={url}
|
||||||
</div>
|
width={40}
|
||||||
))}
|
height={40}
|
||||||
</div>
|
style={{ objectFit: 'cover', borderRadius: '4px', cursor: 'pointer' }}
|
||||||
</Image.PreviewGroup>
|
previewMask={() => <EyeOutlined />}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 选手表格列 */
|
/** 选手表格基础列(不含证件号/证件图片,由 setup 内 computed 按需拼接) */
|
||||||
const PLAYER_COLUMNS = [
|
const BASE_PLAYER_COLUMNS: any[] = [
|
||||||
{ title: '选手', dataIndex: 'realName', key: 'realName', width: 100 },
|
{ title: '选手', dataIndex: 'realName', key: 'realName', width: 100 },
|
||||||
{
|
{
|
||||||
title: '性别',
|
title: '性别',
|
||||||
@@ -57,21 +62,30 @@ const PLAYER_COLUMNS = [
|
|||||||
customRender: ({ text }: { text: string }) => formatGender(text),
|
customRender: ({ text }: { text: string }) => formatGender(text),
|
||||||
},
|
},
|
||||||
{ title: '手机号', dataIndex: 'phone', key: 'phone', width: 130 },
|
{ title: '手机号', dataIndex: 'phone', key: 'phone', width: 130 },
|
||||||
{
|
];
|
||||||
title: '证件号',
|
|
||||||
dataIndex: 'idCard',
|
const ID_CARD_COLUMN: any = {
|
||||||
key: 'idCard',
|
title: '证件号',
|
||||||
width: 180,
|
dataIndex: 'idCard',
|
||||||
customRender: ({ record }: { record?: TournamentAdminInfoSignupVO }) =>
|
key: 'idCard',
|
||||||
record?.idCard ? record.idCard : <span>--</span>,
|
width: 180,
|
||||||
},
|
customRender: ({ record }: { record?: TournamentAdminInfoSignupVO }) =>
|
||||||
{
|
record?.idCard ? record.idCard : <span>--</span>,
|
||||||
title: '证件图片',
|
};
|
||||||
key: 'idCardImgs',
|
|
||||||
width: 280,
|
const ID_CARD_IMG_COLUMN: any = {
|
||||||
customRender: ({ record }: { record?: TournamentAdminInfoSignupVO }) =>
|
title: '证件图片',
|
||||||
record ? renderIdImages(record.idCardImgList || [], record.idCardImg || '') : <span>--</span>,
|
key: 'idCardImgs',
|
||||||
},
|
width: 120,
|
||||||
|
customRender: ({ record }: { record?: TournamentAdminInfoSignupVO }) =>
|
||||||
|
record ? renderIdImages(record.idCardImgList || [], record.idCardImg || '') : <span>--</span>,
|
||||||
|
};
|
||||||
|
|
||||||
|
type ConditionalColumnKey = 'isRequireIdCardNo' | 'isRequireIdCardImg';
|
||||||
|
|
||||||
|
const CONDITIONAL_PLAYER_COLUMNS: { key: ConditionalColumnKey; column: any }[] = [
|
||||||
|
{ key: 'isRequireIdCardNo', column: ID_CARD_COLUMN },
|
||||||
|
{ key: 'isRequireIdCardImg', column: ID_CARD_IMG_COLUMN },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
@@ -86,6 +100,14 @@ export default defineComponent({
|
|||||||
const [detail, setDetail] = useState<TournamentAdminInfoVO | null>(null);
|
const [detail, setDetail] = useState<TournamentAdminInfoVO | null>(null);
|
||||||
const [detailLoading, setDetailLoading] = useState(false);
|
const [detailLoading, setDetailLoading] = useState(false);
|
||||||
|
|
||||||
|
/** 动态列:根据赛事是否要求证件号/证件图片决定显示 */
|
||||||
|
const playerColumns = computed(() => [
|
||||||
|
...BASE_PLAYER_COLUMNS,
|
||||||
|
...CONDITIONAL_PLAYER_COLUMNS.filter(({ key }) => detail.value?.[key] === '1').map(
|
||||||
|
({ column }) => column,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
// ===== 每个组别的选手数据(key = categoryId) =====
|
// ===== 每个组别的选手数据(key = categoryId) =====
|
||||||
const categoryPlayers = ref<Record<string, TournamentAdminInfoSignupVO[]>>({});
|
const categoryPlayers = ref<Record<string, TournamentAdminInfoSignupVO[]>>({});
|
||||||
const categoryPlayerTotal = ref<Record<string, number>>({});
|
const categoryPlayerTotal = ref<Record<string, number>>({});
|
||||||
@@ -149,6 +171,12 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 显示报名起止时间 */
|
||||||
|
const signupTimeDisplay = (startTime?: string, endTime?: string) => {
|
||||||
|
if (!startTime || !endTime) return '--';
|
||||||
|
return `${startTime} 至 ${endTime}`;
|
||||||
|
};
|
||||||
|
|
||||||
// 弹窗打开且 有 record.id 时自动请求(useEffect 带 immediate: true,兼容 destroyOnClose 重建场景)
|
// 弹窗打开且 有 record.id 时自动请求(useEffect 带 immediate: true,兼容 destroyOnClose 重建场景)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (props.visible && props.record?.id) {
|
if (props.visible && props.record?.id) {
|
||||||
@@ -160,7 +188,7 @@ export default defineComponent({
|
|||||||
<Modal
|
<Modal
|
||||||
visible={props.visible}
|
visible={props.visible}
|
||||||
title="查看详情"
|
title="查看详情"
|
||||||
width={900}
|
width={980}
|
||||||
onCancel={props.onClose}
|
onCancel={props.onClose}
|
||||||
footer={null}
|
footer={null}
|
||||||
centered
|
centered
|
||||||
@@ -177,26 +205,32 @@ export default defineComponent({
|
|||||||
<div class={styles.section}>
|
<div class={styles.section}>
|
||||||
<div class={styles.sectionTitle}>基础信息</div>
|
<div class={styles.sectionTitle}>基础信息</div>
|
||||||
<Descriptions size="small" bordered column={3} class={styles.desc}>
|
<Descriptions size="small" bordered column={3} class={styles.desc}>
|
||||||
<Descriptions.Item label="赛事名称">{detail.value.name}</Descriptions.Item>
|
<Descriptions.Item label="赛事名称" span={2}>
|
||||||
<Descriptions.Item label="赛事时间" span={2}>
|
{detail.value.name}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="赛事时间">
|
||||||
{detail.value.startTimeBegin} 至 {detail.value.startTimeEnd}
|
{detail.value.startTimeBegin} 至 {detail.value.startTimeEnd}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
|
|
||||||
<Descriptions.Item label="赛事说明" span={3}>
|
<Descriptions.Item label="赛事说明" span={3}>
|
||||||
{detail.value.descr}
|
<div style={{ whiteSpace: 'pre-wrap', maxHeight: '280px', overflowY: 'auto' }}>
|
||||||
|
{detail.value.descr}
|
||||||
|
</div>
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
|
|
||||||
<Descriptions.Item label="报名起止时间" span={3}>
|
<Descriptions.Item label="报名起止时间" span={3} class={styles.wideLabel}>
|
||||||
{detail.value.signupTimeBegin} 至 {detail.value.signupTimeEnd}
|
{signupTimeDisplay(detail.value.signupTimeBegin, detail.value.signupTimeEnd)}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
|
|
||||||
<Descriptions.Item label="省市区">{detail.value.venueArea}</Descriptions.Item>
|
<Descriptions.Item label="省市区" class={styles.wideCol}>
|
||||||
|
{detail.value.venueArea}
|
||||||
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="地点">{detail.value.venue}</Descriptions.Item>
|
<Descriptions.Item label="地点">{detail.value.venue}</Descriptions.Item>
|
||||||
<Descriptions.Item label="场地编号" class={styles.statCell}>
|
<Descriptions.Item label="场地编号" class={styles.statCell}>
|
||||||
{detail.value.venueNo || '--'}
|
{detail.value.venueNo || '--'}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
|
|
||||||
<Descriptions.Item label="公开活动">
|
<Descriptions.Item label="公开活动" class={styles.wideCol}>
|
||||||
{formatPublic(detail.value.isPublic)}
|
{formatPublic(detail.value.isPublic)}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="证件号">
|
<Descriptions.Item label="证件号">
|
||||||
@@ -206,12 +240,16 @@ export default defineComponent({
|
|||||||
{formatYN(detail.value.isRequireIdCardImg)}
|
{formatYN(detail.value.isRequireIdCardImg)}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
|
|
||||||
<Descriptions.Item label="创建时间">{detail.value.createTime}</Descriptions.Item>
|
<Descriptions.Item label="创建时间" class={styles.wideCol}>
|
||||||
|
{detail.value.createTime}
|
||||||
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="创建人" span={2}>
|
<Descriptions.Item label="创建人" span={2}>
|
||||||
{detail.value.createNickname}
|
{detail.value.createNickname}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
|
|
||||||
<Descriptions.Item label="报名人数">{detail.value.signupCount}</Descriptions.Item>
|
<Descriptions.Item label="报名人数" class={styles.wideCol}>
|
||||||
|
{detail.value.signupCount}
|
||||||
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="退赛人数">{detail.value.dropoutCount}</Descriptions.Item>
|
<Descriptions.Item label="退赛人数">{detail.value.dropoutCount}</Descriptions.Item>
|
||||||
<Descriptions.Item
|
<Descriptions.Item
|
||||||
label="最大人数"
|
label="最大人数"
|
||||||
@@ -220,7 +258,7 @@ export default defineComponent({
|
|||||||
{detail.value.totalLimit || '--'}
|
{detail.value.totalLimit || '--'}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
|
|
||||||
<Descriptions.Item label="总对阵数">
|
<Descriptions.Item label="总对阵数" class={styles.wideCol}>
|
||||||
{detail.value.matchTotalCount}
|
{detail.value.matchTotalCount}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="已完成对阵数" span={2}>
|
<Descriptions.Item label="已完成对阵数" span={2}>
|
||||||
@@ -230,12 +268,16 @@ export default defineComponent({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ===== 封面 ===== */}
|
{/* ===== 封面 ===== */}
|
||||||
{detail.value.img && (
|
<div class={styles.section}>
|
||||||
<div class={styles.section}>
|
<div class={styles.sectionTitle}>封面</div>
|
||||||
<div class={styles.sectionTitle}>封面</div>
|
<div class={styles.coverWrapper}>
|
||||||
<Image style={{ maxHeight: '380px' }} src={detail.value.img} />
|
<Image
|
||||||
|
style={{ maxHeight: '380px' }}
|
||||||
|
src={detail.value.img || defaultCover}
|
||||||
|
fallback={defaultCover}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
{/* ===== 赛事公告 ===== */}
|
{/* ===== 赛事公告 ===== */}
|
||||||
{detail.value.notice && (
|
{detail.value.notice && (
|
||||||
@@ -249,21 +291,20 @@ export default defineComponent({
|
|||||||
{detail.value.categoryList && detail.value.categoryList.length > 0 && (
|
{detail.value.categoryList && detail.value.categoryList.length > 0 && (
|
||||||
<div class={styles.section}>
|
<div class={styles.section}>
|
||||||
<div class={styles.sectionTitle}>组别信息</div>
|
<div class={styles.sectionTitle}>组别信息</div>
|
||||||
{detail.value.categoryList.map((cat: InnerCategoryVO) => (
|
{detail.value.categoryList.map((cat: InnerCategoryVO, idx: number) => (
|
||||||
<div key={cat.id} class={styles.group}>
|
<div key={cat.id} class={styles.group}>
|
||||||
<div class={styles.groupTitle}>
|
<div class={styles.groupTitle}>
|
||||||
{cat.name}({CATEGORY_TYPE_MAP[cat.type] ?? cat.type})
|
组别{idx + 1}:{cat.name}({CATEGORY_TYPE_MAP[cat.type] ?? cat.type})
|
||||||
</div>
|
|
||||||
<div class={styles.tableWrap}>
|
|
||||||
<Table
|
|
||||||
columns={PLAYER_COLUMNS}
|
|
||||||
dataSource={categoryPlayers.value[cat.id] || []}
|
|
||||||
loading={categoryPlayerLoading.value[cat.id]}
|
|
||||||
size="small"
|
|
||||||
pagination={false}
|
|
||||||
bordered
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<Table
|
||||||
|
columns={playerColumns.value}
|
||||||
|
dataSource={categoryPlayers.value[cat.id] || []}
|
||||||
|
loading={categoryPlayerLoading.value[cat.id]}
|
||||||
|
size="small"
|
||||||
|
pagination={false}
|
||||||
|
bordered
|
||||||
|
scroll={{ x: 'max-content' }}
|
||||||
|
/>
|
||||||
{categoryPlayerTotal.value[cat.id] > 0 && (
|
{categoryPlayerTotal.value[cat.id] > 0 && (
|
||||||
<div class={styles.paginationRow}>
|
<div class={styles.paginationRow}>
|
||||||
<Pagination
|
<Pagination
|
||||||
|
|||||||
@@ -25,6 +25,8 @@
|
|||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid #f0f0f0;
|
border: 1px solid #f0f0f0;
|
||||||
|
max-height: 360px;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.imageGrid {
|
.imageGrid {
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export default defineComponent({
|
|||||||
const renderAction = (record: any) => {
|
const renderAction = (record: any) => {
|
||||||
const status = record.status;
|
const status = record.status;
|
||||||
|
|
||||||
if (status === '0' || status === '1') {
|
if (['0', '1', '2'].includes(status)) {
|
||||||
return (
|
return (
|
||||||
<Space>
|
<Space>
|
||||||
<Button type="link" size="small" onClick={() => openDetailModal(record)}>
|
<Button type="link" size="small" onClick={() => openDetailModal(record)}>
|
||||||
|
|||||||
@@ -96,7 +96,10 @@ export function useEventListModel() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSearch = () => fetchList();
|
const handleSearch = () => {
|
||||||
|
pagination.reset();
|
||||||
|
fetchList();
|
||||||
|
};
|
||||||
|
|
||||||
const handleReset = async () => {
|
const handleReset = async () => {
|
||||||
Object.assign(filterForm, FILTER_DEFAULTS);
|
Object.assign(filterForm, FILTER_DEFAULTS);
|
||||||
@@ -142,8 +145,8 @@ export function useEventListModel() {
|
|||||||
const isRemoved = record.status === '4';
|
const isRemoved = record.status === '4';
|
||||||
const actionText = isRemoved ? '上架' : '下架';
|
const actionText = isRemoved ? '上架' : '下架';
|
||||||
const descText = isRemoved
|
const descText = isRemoved
|
||||||
? `确认上架"${record.name}"吗?上架后用户可搜索和报名该赛事。`
|
? `确认上架"${record.name}"该赛事吗?`
|
||||||
: `确认下架"${record.name}"吗?下架后用户将无法搜索和报名该赛事。`;
|
: `确认下架"${record.name}"吗?下架后用户将无法搜索和报名该赛事,已有报名和订单不受影响。`;
|
||||||
|
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: `${actionText}确认`,
|
title: `${actionText}确认`,
|
||||||
@@ -154,14 +157,14 @@ export function useEventListModel() {
|
|||||||
setTogglingId(record.id);
|
setTogglingId(record.id);
|
||||||
try {
|
try {
|
||||||
const res = await toggleEventOnline({ id: record.id, online: isRemoved ? '1' : '0' });
|
const res = await toggleEventOnline({ id: record.id, online: isRemoved ? '1' : '0' });
|
||||||
if (res.code === 200) {
|
if (res.code == 200) {
|
||||||
message.success(`${actionText}成功`);
|
message.success(`${actionText}成功`);
|
||||||
fetchList();
|
fetchList();
|
||||||
} else {
|
} else {
|
||||||
message.error(res.msg || `${actionText}失败`);
|
message.error(res.msg || `${actionText}失败`);
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
message.error(e?.message || `${actionText}失败`);
|
console.error(`${actionText}失败:`, e);
|
||||||
} finally {
|
} finally {
|
||||||
setTogglingId('');
|
setTogglingId('');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
Form,
|
Form,
|
||||||
Space,
|
Space,
|
||||||
Select,
|
Select,
|
||||||
Tooltip,
|
Modal,
|
||||||
Pagination,
|
Pagination,
|
||||||
} from 'ant-design-vue';
|
} from 'ant-design-vue';
|
||||||
import { useLogModel, ACTION_TYPE_OPTIONS, ACTION_SOURCE_OPTIONS } from './model/useLogModel';
|
import { useLogModel, ACTION_TYPE_OPTIONS, ACTION_SOURCE_OPTIONS } from './model/useLogModel';
|
||||||
@@ -24,6 +24,7 @@ const LogContentCell = defineComponent({
|
|||||||
setup(props) {
|
setup(props) {
|
||||||
const textRef = ref<HTMLElement | null>(null);
|
const textRef = ref<HTMLElement | null>(null);
|
||||||
const [overflow, setOverflow] = useState(false);
|
const [overflow, setOverflow] = useState(false);
|
||||||
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
const checkOverflow = () => {
|
const checkOverflow = () => {
|
||||||
const el = textRef.value;
|
const el = textRef.value;
|
||||||
if (el) setOverflow(el.scrollHeight > el.clientHeight);
|
if (el) setOverflow(el.scrollHeight > el.clientHeight);
|
||||||
@@ -47,10 +48,23 @@ const LogContentCell = defineComponent({
|
|||||||
{raw}
|
{raw}
|
||||||
</div>
|
</div>
|
||||||
{overflow.value ? (
|
{overflow.value ? (
|
||||||
<Tooltip title={raw} placement="topLeft">
|
<span class="log-content-view" onClick={() => setModalOpen(true)}>
|
||||||
<span class="log-content-view">查看</span>
|
查看
|
||||||
</Tooltip>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
|
<Modal
|
||||||
|
visible={modalOpen.value}
|
||||||
|
title="操作内容"
|
||||||
|
footer={null}
|
||||||
|
width={600}
|
||||||
|
centered
|
||||||
|
destroyOnClose
|
||||||
|
onCancel={() => setModalOpen(false)}
|
||||||
|
>
|
||||||
|
<div style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all', lineHeight: 1.8 }}>
|
||||||
|
{raw}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -93,7 +107,7 @@ export default defineComponent({
|
|||||||
record: any;
|
record: any;
|
||||||
index: number;
|
index: number;
|
||||||
}) => {
|
}) => {
|
||||||
if (column.key === 'content') {
|
if (column.key === 'obj') {
|
||||||
const raw = text || '';
|
const raw = text || '';
|
||||||
return raw ? <LogContentCell text={raw} /> : <span>--</span>;
|
return raw ? <LogContentCell text={raw} /> : <span>--</span>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,7 +120,10 @@ export function useLogModel() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSearch = () => fetchList();
|
const handleSearch = () => {
|
||||||
|
pagination.reset();
|
||||||
|
fetchList();
|
||||||
|
};
|
||||||
|
|
||||||
const handleReset = async () => {
|
const handleReset = async () => {
|
||||||
filterForm.dateRange = null;
|
filterForm.dateRange = null;
|
||||||
|
|||||||
@@ -79,11 +79,6 @@
|
|||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tableWrap {
|
|
||||||
max-height: 200px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.paginationRow {
|
.paginationRow {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
@@ -160,12 +155,6 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.partialMark {
|
|
||||||
margin-left: 4px;
|
|
||||||
color: #fa8c16;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.refundEmpty {
|
.refundEmpty {
|
||||||
padding: 32px 0;
|
padding: 32px 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -176,10 +165,8 @@
|
|||||||
border: 1px dashed #e0e0e0;
|
border: 1px dashed #e0e0e0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.refundPagination {
|
.refundPlayers {
|
||||||
display: flex;
|
margin-left: 0;
|
||||||
justify-content: flex-end;
|
|
||||||
margin-top: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modalFooter {
|
.modalFooter {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { defineComponent, computed } from 'vue';
|
import { defineComponent, computed } from 'vue';
|
||||||
import { Modal, Table, Button, Pagination, Image, Space, Spin } from 'ant-design-vue';
|
import { Modal, Table, Button, Pagination, Image, Space, Spin } from 'ant-design-vue';
|
||||||
|
import { EyeOutlined } from '@ant-design/icons-vue';
|
||||||
import { StatusTag, type StatusTagTone } from '@/components';
|
import { StatusTag, type StatusTagTone } from '@/components';
|
||||||
import { useState, useEffect } from '@/hooks';
|
import { useState, useEffect } from '@/hooks';
|
||||||
import {
|
import {
|
||||||
@@ -8,6 +9,7 @@ import {
|
|||||||
type TournamentAdminOrderInfoVO,
|
type TournamentAdminOrderInfoVO,
|
||||||
type InnerRefundInfo,
|
type InnerRefundInfo,
|
||||||
type TournamentAdminOrderInfoPageVO,
|
type TournamentAdminOrderInfoPageVO,
|
||||||
|
OrderDetailQueryParams,
|
||||||
} from '../model/services';
|
} from '../model/services';
|
||||||
import styles from './OrderDetailModal.module.less';
|
import styles from './OrderDetailModal.module.less';
|
||||||
|
|
||||||
@@ -44,22 +46,31 @@ const PLAYER_COLUMNS = [
|
|||||||
customRender: ({ text }: { text: string }) => formatGender(text),
|
customRender: ({ text }: { text: string }) => formatGender(text),
|
||||||
},
|
},
|
||||||
{ title: '手机号', dataIndex: 'phone', key: 'phone', width: 130 },
|
{ title: '手机号', dataIndex: 'phone', key: 'phone', width: 130 },
|
||||||
{ title: '证件号', dataIndex: 'idCard', key: 'idCard', width: 180 },
|
{
|
||||||
|
title: '证件号',
|
||||||
|
dataIndex: 'idCard',
|
||||||
|
key: 'idCard',
|
||||||
|
width: 180,
|
||||||
|
customRender: ({ text }: { text: string }) => text || '--',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '证件图片',
|
title: '证件图片',
|
||||||
key: 'idCardImg',
|
key: 'idCardImg',
|
||||||
width: 170,
|
width: 170,
|
||||||
customRender: ({ record }: { record: TournamentAdminOrderInfoPageVO }) => {
|
customRender: ({ record }: { record: TournamentAdminOrderInfoPageVO }) => {
|
||||||
const urls = record.idCardImgList?.length
|
const urls = record.idCardImgList || [];
|
||||||
? record.idCardImgList
|
|
||||||
: record.idCardImg
|
|
||||||
? [record.idCardImg]
|
|
||||||
: [];
|
|
||||||
if (!urls.length) return <span>--</span>;
|
if (!urls.length) return <span>--</span>;
|
||||||
return (
|
return (
|
||||||
<Space>
|
<Space>
|
||||||
{urls.map((src) => (
|
{urls.map((src) => (
|
||||||
<Image key={src} width={60} height={40} src={src} />
|
<Image
|
||||||
|
key={src}
|
||||||
|
width={60}
|
||||||
|
height={40}
|
||||||
|
src={src}
|
||||||
|
style={{ objectFit: 'cover', borderRadius: '4px', cursor: 'pointer' }}
|
||||||
|
previewMask={() => <EyeOutlined />}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
@@ -108,8 +119,9 @@ const PLAYER_COLUMNS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const REFUND_STATUS_MAP: Record<string, { label: string; tone: StatusTagTone }> = {
|
const REFUND_STATUS_MAP: Record<string, { label: string; tone: StatusTagTone }> = {
|
||||||
success: { label: '退款成功', tone: 'success' },
|
'1': { label: '退款中', tone: 'orange' },
|
||||||
failed: { label: '退款失败', tone: 'danger' },
|
'2': { label: '退款成功', tone: 'success' },
|
||||||
|
'3': { label: '退款失败', tone: 'danger' },
|
||||||
};
|
};
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
@@ -129,8 +141,6 @@ export default defineComponent({
|
|||||||
const [playersLoading, setPlayersLoading] = useState(true);
|
const [playersLoading, setPlayersLoading] = useState(true);
|
||||||
const [playerPage, setPlayerPage] = useState(1);
|
const [playerPage, setPlayerPage] = useState(1);
|
||||||
const [playerTotal, setPlayerTotal] = useState(0);
|
const [playerTotal, setPlayerTotal] = useState(0);
|
||||||
const [refundPage, setRefundPage] = useState(1);
|
|
||||||
const [refundPageSize] = useState(2);
|
|
||||||
const playerPageSize = 5;
|
const playerPageSize = 5;
|
||||||
|
|
||||||
/** 获取选手列表(分页) */
|
/** 获取选手列表(分页) */
|
||||||
@@ -138,8 +148,7 @@ export default defineComponent({
|
|||||||
setPlayersLoading(true);
|
setPlayersLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await getOrderPlayerList({
|
const res = await getOrderPlayerList({
|
||||||
orderNo: props.orderNo,
|
...(props.orderNo ? { orderNo: props.orderNo } : { uniqueId: props.uniqueId }),
|
||||||
uniqueId: props.uniqueId,
|
|
||||||
page: String(page),
|
page: String(page),
|
||||||
limit: String(playerPageSize),
|
limit: String(playerPageSize),
|
||||||
});
|
});
|
||||||
@@ -158,27 +167,26 @@ export default defineComponent({
|
|||||||
const hasRefundFailed = computed(() => {
|
const hasRefundFailed = computed(() => {
|
||||||
const records = refundRecords.value;
|
const records = refundRecords.value;
|
||||||
if (!records || !records.length) return false;
|
if (!records || !records.length) return false;
|
||||||
return records.some((r) => {
|
return records.some((r) => r?.refundStatus === '3');
|
||||||
if (!r) return false;
|
|
||||||
const hasRefundAmount = parseFloat(r.refundAmount || '0') > 0;
|
|
||||||
const hasRefundTime = r.refundTime && r.refundTime !== '--';
|
|
||||||
return hasRefundAmount && !hasRefundTime;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const pagedRefunds = computed(() => {
|
const refundSuccessList = computed(() =>
|
||||||
const start = (refundPage.value - 1) * refundPageSize.value;
|
refundRecords.value.filter((r) => r.refundStatus === '2'),
|
||||||
return refundRecords.value.slice(start, start + refundPageSize.value);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
const totalRefunded = computed(() => {
|
const totalRefunded = computed(() => {
|
||||||
const records = refundRecords.value;
|
const list = refundSuccessList.value;
|
||||||
if (!records.length) return 0;
|
if (!list.length) return 0;
|
||||||
return records.reduce((sum, r) => sum + parseFloat(r.refundAmount || '0'), 0);
|
return list.reduce((sum, r) => sum + parseFloat(r.refundAmount || '0'), 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const refundSuccessCount = computed(() => refundSuccessList.value.length);
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
const params = { orderNo: props.orderNo, uniqueId: props.uniqueId };
|
const params: OrderDetailQueryParams = {};
|
||||||
|
if (props.orderNo) params['orderNo'] = props.orderNo;
|
||||||
|
else params['uniqueId'] = props.uniqueId;
|
||||||
|
|
||||||
const [detailRes] = await Promise.all([getOrderDetail(params), fetchPlayers(1)]);
|
const [detailRes] = await Promise.all([getOrderDetail(params), fetchPlayers(1)]);
|
||||||
if (detailRes.code == 200) {
|
if (detailRes.code == 200) {
|
||||||
setDetail(detailRes.data);
|
setDetail(detailRes.data);
|
||||||
@@ -194,10 +202,6 @@ export default defineComponent({
|
|||||||
fetchPlayers(page);
|
fetchPlayers(page);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRefundPageChange = (page: number) => {
|
|
||||||
setRefundPage(page);
|
|
||||||
};
|
|
||||||
|
|
||||||
return () => (
|
return () => (
|
||||||
<Modal
|
<Modal
|
||||||
visible={props.visible}
|
visible={props.visible}
|
||||||
@@ -260,7 +264,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
<div class={styles.section}>
|
<div class={styles.section}>
|
||||||
<div class={styles.sectionTitle}>选手信息</div>
|
<div class={styles.sectionTitle}>选手信息</div>
|
||||||
<div class={styles.tableWrap}>
|
<div>
|
||||||
<Table
|
<Table
|
||||||
columns={PLAYER_COLUMNS}
|
columns={PLAYER_COLUMNS}
|
||||||
dataSource={players.value}
|
dataSource={players.value}
|
||||||
@@ -268,7 +272,7 @@ export default defineComponent({
|
|||||||
size="small"
|
size="small"
|
||||||
bordered
|
bordered
|
||||||
pagination={false}
|
pagination={false}
|
||||||
scroll={{ x: 'max-content' }}
|
scroll={{ x: 'max-content', y: 200 }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class={styles.paginationRow}>
|
<div class={styles.paginationRow}>
|
||||||
@@ -291,28 +295,18 @@ export default defineComponent({
|
|||||||
<>
|
<>
|
||||||
<div class={styles.refundSummary}>
|
<div class={styles.refundSummary}>
|
||||||
<span>已退金额:{totalRefunded.value.toFixed(2)}元</span>
|
<span>已退金额:{totalRefunded.value.toFixed(2)}元</span>
|
||||||
<span>退款次数:{refundRecords.value.length}次</span>
|
<span>退款次数:{refundSuccessCount.value}次</span>
|
||||||
</div>
|
</div>
|
||||||
<div class={styles.refundList}>
|
<div class={styles.refundList}>
|
||||||
{pagedRefunds.value.map((r, idx) => {
|
{refundRecords.value.map((r, idx) => {
|
||||||
const realIdx = (refundPage.value - 1) * refundPageSize.value + idx + 1;
|
const statusInfo = REFUND_STATUS_MAP[r.refundStatus] || {
|
||||||
const isSuccess = parseFloat(r.refundTime || '') > 0 || !!r.refundTime;
|
label: '未知',
|
||||||
const statusInfo = {
|
tone: 'default' as StatusTagTone,
|
||||||
label: isSuccess
|
|
||||||
? '退款成功'
|
|
||||||
: r.isFullRefund === '1'
|
|
||||||
? '退款待处理'
|
|
||||||
: '退款中',
|
|
||||||
tone: (isSuccess
|
|
||||||
? 'success'
|
|
||||||
: r.isFullRefund === '1'
|
|
||||||
? 'warning'
|
|
||||||
: 'orange') as StatusTagTone,
|
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div key={r.refundOutTradeNo || idx} class={styles.refundCard}>
|
<div key={r.refundOutTradeNo || idx} class={styles.refundCard}>
|
||||||
<div class={styles.refundCardHeader}>
|
<div class={styles.refundCardHeader}>
|
||||||
<span class={styles.refundIndex}>第{realIdx}次退款</span>
|
<span class={styles.refundIndex}>第{idx + 1}次退款</span>
|
||||||
<StatusTag label={statusInfo.label} tone={statusInfo.tone} />
|
<StatusTag label={statusInfo.label} tone={statusInfo.tone} />
|
||||||
</div>
|
</div>
|
||||||
<div class={styles.refundCardBody}>
|
<div class={styles.refundCardBody}>
|
||||||
@@ -321,37 +315,23 @@ export default defineComponent({
|
|||||||
退款金额:
|
退款金额:
|
||||||
<span class={styles.refundAmount}>
|
<span class={styles.refundAmount}>
|
||||||
¥{formatMoney(r.refundAmount)}
|
¥{formatMoney(r.refundAmount)}
|
||||||
{r.isFullRefund === '1' && (
|
|
||||||
<span class={styles.partialMark}>(全部退款)</span>
|
|
||||||
)}
|
|
||||||
{r.isFullRefund === '0' && (
|
|
||||||
<span class={styles.partialMark}>(部分退款)</span>
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
|
<span
|
||||||
|
class={styles.refundPlayers}
|
||||||
|
>{`(${r.refundSignupNames})`}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>申请时间:{r.refundApplyTime}</div>
|
<div>申请时间:{r.refundApplyTime}</div>
|
||||||
<div>完款时间:{r.refundTime || '--'}</div>
|
<div>完成时间:{r.refundTime || '--'}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class={styles.refundCol}>
|
<div class={styles.refundCol}>
|
||||||
<div>退款方式:{r.refundMethod}</div>
|
<div>退款方式:{r.refundMethod}</div>
|
||||||
<div>第三方退款单号:{r.refundOutTradeNo || '--'}</div>
|
<div>第三方退款单号:{r.refundOutTradeNo || '--'}</div>
|
||||||
<div>已退金额:¥{formatMoney(r.refundedAmount)}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div class={styles.refundPagination}>
|
|
||||||
<Pagination
|
|
||||||
current={refundPage.value}
|
|
||||||
pageSize={refundPageSize.value}
|
|
||||||
total={refundRecords.value.length}
|
|
||||||
showSizeChanger={false}
|
|
||||||
size="small"
|
|
||||||
onChange={handleRefundPageChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div class={styles.refundEmpty}>暂无退款记录</div>
|
<div class={styles.refundEmpty}>暂无退款记录</div>
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ function renderBodyCell({
|
|||||||
if (column.dataIndex === 'name') {
|
if (column.dataIndex === 'name') {
|
||||||
const maxLen = 15;
|
const maxLen = 15;
|
||||||
const raw = text || '';
|
const raw = text || '';
|
||||||
|
if (!raw) return <span>--</span>;
|
||||||
const display = raw.length > maxLen ? raw.slice(0, maxLen) + '...' : raw;
|
const display = raw.length > maxLen ? raw.slice(0, maxLen) + '...' : raw;
|
||||||
return raw.length > maxLen ? (
|
return raw.length > maxLen ? (
|
||||||
<Tooltip title={raw}>
|
<Tooltip title={raw}>
|
||||||
|
|||||||
@@ -125,22 +125,77 @@ export function useOrderModel() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ title: '订单编号', dataIndex: 'orderNo', key: 'orderNo', width: 200 },
|
{
|
||||||
{ title: '赛事名称', dataIndex: 'name', key: 'name', width: 180 },
|
title: '订单编号',
|
||||||
{ title: '选手名称', dataIndex: 'realName', key: 'realName', width: 100 },
|
dataIndex: 'orderNo',
|
||||||
{ title: '手机号', dataIndex: 'phone', key: 'phone', width: 130 },
|
key: 'orderNo',
|
||||||
{ title: '比赛组别', dataIndex: 'categoryName', key: 'categoryName', width: 120 },
|
width: 200,
|
||||||
{ title: '报名人', dataIndex: 'creatorName', key: 'creatorName', width: 100 },
|
customRender: ({ text }: { text: string }) => text || '--',
|
||||||
{ title: '报名人手机号', dataIndex: 'creatorPhone', key: 'creatorPhone', width: 130 },
|
},
|
||||||
|
{
|
||||||
|
title: '赛事名称',
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
width: 180,
|
||||||
|
customRender: ({ text }: { text: string }) => text || '--',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '选手名称',
|
||||||
|
dataIndex: 'realName',
|
||||||
|
key: 'realName',
|
||||||
|
width: 100,
|
||||||
|
customRender: ({ text }: { text: string }) => text || '--',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '手机号',
|
||||||
|
dataIndex: 'phone',
|
||||||
|
key: 'phone',
|
||||||
|
width: 130,
|
||||||
|
customRender: ({ text }: { text: string }) => text || '--',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '比赛组别',
|
||||||
|
dataIndex: 'categoryName',
|
||||||
|
key: 'categoryName',
|
||||||
|
width: 120,
|
||||||
|
customRender: ({ text }: { text: string }) => text || '--',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '报名人',
|
||||||
|
dataIndex: 'creatorName',
|
||||||
|
key: 'creatorName',
|
||||||
|
width: 100,
|
||||||
|
customRender: ({ text }: { text: string }) => text || '--',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '报名人手机号',
|
||||||
|
dataIndex: 'creatorPhone',
|
||||||
|
key: 'creatorPhone',
|
||||||
|
width: 130,
|
||||||
|
customRender: ({ text }: { text: string }) => text || '--',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '报名人数',
|
title: '报名人数',
|
||||||
dataIndex: 'creatorSignupCount',
|
dataIndex: 'creatorSignupCount',
|
||||||
key: 'creatorSignupCount',
|
key: 'creatorSignupCount',
|
||||||
width: 90,
|
width: 90,
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
|
customRender: ({ text }: { text: number }) => (text != null ? text : '--'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '订单时间',
|
||||||
|
dataIndex: 'orderTime',
|
||||||
|
key: 'orderTime',
|
||||||
|
width: 170,
|
||||||
|
customRender: ({ text }: { text: string }) => text || '--',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '支付时间',
|
||||||
|
dataIndex: 'payTime',
|
||||||
|
key: 'payTime',
|
||||||
|
width: 170,
|
||||||
|
customRender: ({ text }: { text: string }) => text || '--',
|
||||||
},
|
},
|
||||||
{ title: '订单时间', dataIndex: 'orderTime', key: 'orderTime', width: 170 },
|
|
||||||
{ title: '支付时间', dataIndex: 'payTime', key: 'payTime', width: 170 },
|
|
||||||
{
|
{
|
||||||
title: '订单总金额',
|
title: '订单总金额',
|
||||||
dataIndex: 'creatorSignupFee',
|
dataIndex: 'creatorSignupFee',
|
||||||
@@ -199,7 +254,10 @@ export function useOrderModel() {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleSearch = () => fetchList();
|
const handleSearch = () => {
|
||||||
|
pagination.reset();
|
||||||
|
fetchList();
|
||||||
|
};
|
||||||
|
|
||||||
const handleReset = async () => {
|
const handleReset = async () => {
|
||||||
Object.assign(filterForm, FILTER_DEFAULTS);
|
Object.assign(filterForm, FILTER_DEFAULTS);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import pageStyles from '@/assets/styles/pageLayout.module.less';
|
|||||||
* 架构分层:
|
* 架构分层:
|
||||||
* - 数据层:services.ts(API 调用)
|
* - 数据层:services.ts(API 调用)
|
||||||
* - 逻辑层:useUserModel.ts(状态 + 业务逻辑 + 交互处理)
|
* - 逻辑层:useUserModel.ts(状态 + 业务逻辑 + 交互处理)
|
||||||
* - 视图层:useUserColumns.ts(表格列配置)+ 本组件(纯 UI 渲染)
|
* - 视图层:useUserColumns.tsx(表格列配置)+ 本组件(纯 UI 渲染)
|
||||||
*/
|
*/
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'EventUsers',
|
name: 'EventUsers',
|
||||||
@@ -46,7 +46,7 @@ export default defineComponent({
|
|||||||
loading={togglingId.value === record.id}
|
loading={togglingId.value === record.id}
|
||||||
onClick={() => handleToggleStatus(record)}
|
onClick={() => handleToggleStatus(record)}
|
||||||
>
|
>
|
||||||
{isActive ? '停用' : '启用'}
|
{isActive ? '禁用' : '启用'}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -79,6 +79,16 @@ export default defineComponent({
|
|||||||
onPressEnter={handleSearch}
|
onPressEnter={handleSearch}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item label="手机号" name="phone">
|
||||||
|
<Input
|
||||||
|
value={filterForm.phone}
|
||||||
|
placeholder="请输入手机号"
|
||||||
|
style={{ width: '180px' }}
|
||||||
|
allowClear
|
||||||
|
onUpdate:value={(val: any) => (filterForm.phone = val.target?.value ?? val ?? '')}
|
||||||
|
onPressEnter={handleSearch}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
<Form.Item label="状态" name="status">
|
<Form.Item label="状态" name="status">
|
||||||
<Select
|
<Select
|
||||||
value={filterForm.status}
|
value={filterForm.status}
|
||||||
|
|||||||
@@ -3,12 +3,14 @@
|
|||||||
* 集中管理筛选默认值、字段映射、选项配置
|
* 集中管理筛选默认值、字段映射、选项配置
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { StatusTagTone } from '@/components';
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 筛选默认值
|
// 筛选默认值
|
||||||
// ============================================================
|
// ============================================================
|
||||||
export const FILTER_DEFAULTS = {
|
export const FILTER_DEFAULTS = {
|
||||||
text: '',
|
nickname: '',
|
||||||
roleId: '',
|
phone: '',
|
||||||
status: '',
|
status: '',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@@ -19,8 +21,8 @@ export const FILTER_DEFAULTS = {
|
|||||||
type FieldMapping = [keyof typeof FILTER_DEFAULTS, string, ((v: any) => any)?];
|
type FieldMapping = [keyof typeof FILTER_DEFAULTS, string, ((v: any) => any)?];
|
||||||
|
|
||||||
export const FIELD_MAPPINGS: FieldMapping[] = [
|
export const FIELD_MAPPINGS: FieldMapping[] = [
|
||||||
['text', 'text', (v) => v?.trim?.() ?? v],
|
['nickname', 'nickname', (v) => v?.trim?.() ?? v],
|
||||||
['roleId', 'roleId'],
|
['phone', 'phone'],
|
||||||
['status', 'status'],
|
['status', 'status'],
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -29,25 +31,15 @@ export const FIELD_MAPPINGS: FieldMapping[] = [
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
export const USER_STATUS_OPTIONS = [
|
export const USER_STATUS_OPTIONS = [
|
||||||
{ value: '', label: '全部' },
|
{ value: '', label: '全部' },
|
||||||
{ value: '0', label: '停用' },
|
{ value: '0', label: '禁用' },
|
||||||
{ value: '1', label: '正常' },
|
{ value: '1', label: '正常' },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
/** 角色选项(TODO: 对接角色 API) */
|
|
||||||
export const ROLE_OPTIONS = [
|
|
||||||
{ value: '', label: '全部' },
|
|
||||||
{ value: '1', label: '超级管理员' },
|
|
||||||
{ value: '2', label: '赛事管理员' },
|
|
||||||
{ value: '3', label: '裁判' },
|
|
||||||
{ value: '4', label: '财务' },
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 状态映射(用于 StatusTag 渲染)
|
// 状态映射(用于 StatusTag 渲染)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
import type { StatusTagTone } from '@/components';
|
|
||||||
|
|
||||||
export const STATUS_MAP: Record<string, { label: string; tone: StatusTagTone }> = {
|
export const STATUS_MAP: Record<string, { label: string; tone: StatusTagTone }> = {
|
||||||
'1': { label: '正常', tone: 'success' },
|
'1': { label: '正常', tone: 'success' },
|
||||||
'0': { label: '停用', tone: 'danger' },
|
'0': { label: '禁用', tone: 'danger' },
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,14 +18,15 @@ import type {
|
|||||||
|
|
||||||
/** 赛事用户列表项 */
|
/** 赛事用户列表项 */
|
||||||
export interface EventUserVO {
|
export interface EventUserVO {
|
||||||
id: number;
|
id: string;
|
||||||
nickname: string;
|
nickname: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
realName: string;
|
realName: string;
|
||||||
idCard: string;
|
idCard: string;
|
||||||
idCardImg: string;
|
idCardImg: string;
|
||||||
idCardImgList: string[];
|
idCardImgList: string[];
|
||||||
status: number;
|
gender: string;
|
||||||
|
status: string;
|
||||||
createTime: string;
|
createTime: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+27
-24
@@ -1,6 +1,5 @@
|
|||||||
import { h, type VNodeChild } from 'vue';
|
|
||||||
import { Image, Space } from 'ant-design-vue';
|
import { Image, Space } from 'ant-design-vue';
|
||||||
import type { ImageProps } from 'ant-design-vue/es/image';
|
import { EyeOutlined } from '@ant-design/icons-vue';
|
||||||
import { StatusTag } from '@/components';
|
import { StatusTag } from '@/components';
|
||||||
import { STATUS_MAP } from './config';
|
import { STATUS_MAP } from './config';
|
||||||
|
|
||||||
@@ -10,6 +9,9 @@ const GENDER_MAP: Record<string, string> = {
|
|||||||
'2': '女',
|
'2': '女',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 空值占位 */
|
||||||
|
const E = (v: string | undefined | null) => v || '--';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户列表页 - 表格列配置(视图层)
|
* 用户列表页 - 表格列配置(视图层)
|
||||||
*
|
*
|
||||||
@@ -24,6 +26,7 @@ export function useUserColumns() {
|
|||||||
key: 'nickname',
|
key: 'nickname',
|
||||||
width: 140,
|
width: 140,
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
|
customRender: ({ text }: { text: string }) => E(text),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '用户姓名',
|
title: '用户姓名',
|
||||||
@@ -31,6 +34,7 @@ export function useUserColumns() {
|
|||||||
key: 'realName',
|
key: 'realName',
|
||||||
width: 110,
|
width: 110,
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
|
customRender: ({ text }: { text: string }) => E(text),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '手机号',
|
title: '手机号',
|
||||||
@@ -38,6 +42,7 @@ export function useUserColumns() {
|
|||||||
key: 'phone',
|
key: 'phone',
|
||||||
width: 140,
|
width: 140,
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
|
customRender: ({ text }: { text: string }) => E(text),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '性别',
|
title: '性别',
|
||||||
@@ -45,7 +50,7 @@ export function useUserColumns() {
|
|||||||
key: 'gender',
|
key: 'gender',
|
||||||
width: 80,
|
width: 80,
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
customRender: ({ text }: { text: string }) => GENDER_MAP[text] || '',
|
customRender: ({ text }: { text: string }) => GENDER_MAP[text] || '--',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '证件号',
|
title: '证件号',
|
||||||
@@ -53,6 +58,7 @@ export function useUserColumns() {
|
|||||||
key: 'idCard',
|
key: 'idCard',
|
||||||
width: 120,
|
width: 120,
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
|
customRender: ({ text }: { text: string }) => E(text),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '证件图片',
|
title: '证件图片',
|
||||||
@@ -60,26 +66,22 @@ export function useUserColumns() {
|
|||||||
key: 'idCardImgList',
|
key: 'idCardImgList',
|
||||||
width: 120,
|
width: 120,
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
customRender: ({ text }: { text: string[] }): VNodeChild => {
|
customRender: ({ text }: { text: string[] }) => {
|
||||||
const imgList = Array.isArray(text) ? text : [];
|
const imgList = Array.isArray(text) ? text : [];
|
||||||
if (imgList.length === 0) return '--';
|
if (imgList.length === 0) return <span>--</span>;
|
||||||
return h(
|
return (
|
||||||
Space,
|
<Space size={4}>
|
||||||
{ size: 4 },
|
{imgList.map((url, index) => (
|
||||||
{
|
<Image
|
||||||
default: () =>
|
key={index}
|
||||||
imgList.map((url, index) =>
|
src={url}
|
||||||
h<ImageProps>(Image, {
|
width={40}
|
||||||
key: index,
|
height={40}
|
||||||
src: url,
|
style={{ objectFit: 'cover', borderRadius: '4px', cursor: 'pointer' }}
|
||||||
alt: `证件图${index + 1}`,
|
previewMask={() => <EyeOutlined />}
|
||||||
width: 40,
|
/>
|
||||||
height: 40,
|
))}
|
||||||
style: { objectFit: 'cover', borderRadius: '4px', cursor: 'pointer' },
|
</Space>
|
||||||
preview: true,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -89,6 +91,7 @@ export function useUserColumns() {
|
|||||||
key: 'createTime',
|
key: 'createTime',
|
||||||
width: 170,
|
width: 170,
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
|
customRender: ({ text }: { text: string }) => E(text),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '状态',
|
title: '状态',
|
||||||
@@ -97,8 +100,8 @@ export function useUserColumns() {
|
|||||||
width: 90,
|
width: 90,
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
customRender: ({ text }: { text: string }) => {
|
customRender: ({ text }: { text: string }) => {
|
||||||
const info = STATUS_MAP[text] || { label: text, tone: 'default' as const };
|
const info = STATUS_MAP[text] || { label: text || '--', tone: 'default' as const };
|
||||||
return h(StatusTag, { label: info.label, tone: info.tone });
|
return <StatusTag label={info.label} tone={info.tone} />;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -1,13 +1,9 @@
|
|||||||
import { computed, reactive, toRef, Ref, type UnwrapRef } from 'vue';
|
import { computed, reactive } from 'vue';
|
||||||
import { Modal, message } from 'ant-design-vue';
|
import { Modal, message } from 'ant-design-vue';
|
||||||
import { useState, useDebounce } from '@/hooks';
|
import { useState } from '@/hooks';
|
||||||
import { useRequest } from '@/hooks/useRequest';
|
import { useRequest } from '@/hooks/useRequest';
|
||||||
import { useEffect } from '@/hooks/useEffect';
|
import { useEffect } from '@/hooks/useEffect';
|
||||||
import {
|
import { usePagination, syncPaginationTotal } from '@/hooks/usePagination';
|
||||||
usePagination,
|
|
||||||
syncPaginationTotal,
|
|
||||||
type UsePaginationReturn,
|
|
||||||
} from '@/hooks/usePagination';
|
|
||||||
import { hasValue, isEmptyValue, safeTransform } from '@/utils';
|
import { hasValue, isEmptyValue, safeTransform } from '@/utils';
|
||||||
import {
|
import {
|
||||||
getUserList,
|
getUserList,
|
||||||
@@ -17,19 +13,9 @@ import {
|
|||||||
type PageData,
|
type PageData,
|
||||||
type ApiResult,
|
type ApiResult,
|
||||||
} from './services';
|
} from './services';
|
||||||
import {
|
import { FILTER_DEFAULTS, FIELD_MAPPINGS, USER_STATUS_OPTIONS, STATUS_MAP } from './config';
|
||||||
FILTER_DEFAULTS,
|
|
||||||
FIELD_MAPPINGS,
|
|
||||||
USER_STATUS_OPTIONS,
|
|
||||||
ROLE_OPTIONS,
|
|
||||||
STATUS_MAP,
|
|
||||||
} from './config';
|
|
||||||
|
|
||||||
export { USER_STATUS_OPTIONS, ROLE_OPTIONS, STATUS_MAP };
|
export { USER_STATUS_OPTIONS, STATUS_MAP };
|
||||||
|
|
||||||
// 提取 Pagination Ref 类型
|
|
||||||
type PaginationRef = UnwrapRef<UsePaginationReturn>;
|
|
||||||
type PageSizeRef = PaginationRef['pageSize'];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户列表页数据模型
|
* 用户列表页数据模型
|
||||||
@@ -38,7 +24,7 @@ type PageSizeRef = PaginationRef['pageSize'];
|
|||||||
* 架构分层:
|
* 架构分层:
|
||||||
* - 数据层:services.ts(API 调用)
|
* - 数据层:services.ts(API 调用)
|
||||||
* - 逻辑层:本文件(状态 + 业务逻辑)
|
* - 逻辑层:本文件(状态 + 业务逻辑)
|
||||||
* - 视图层:useUserColumns.ts + index.tsx(纯 UI 渲染)
|
* - 视图层:useUserColumns.tsx + index.tsx(纯 UI 渲染)
|
||||||
*/
|
*/
|
||||||
export function useUserModel() {
|
export function useUserModel() {
|
||||||
// ===== 筛选表单状态 =====
|
// ===== 筛选表单状态 =====
|
||||||
@@ -50,11 +36,6 @@ export function useUserModel() {
|
|||||||
// ===== 分页管理 =====
|
// ===== 分页管理 =====
|
||||||
const pagination = usePagination({ defaultCurrent: 1, defaultPageSize: 10 });
|
const pagination = usePagination({ defaultCurrent: 1, defaultPageSize: 10 });
|
||||||
|
|
||||||
// ===== 防抖处理 =====
|
|
||||||
const { debouncedValue: debouncedText } = useDebounce(toRef(filterForm, 'text') as Ref<string>, {
|
|
||||||
delay: 300,
|
|
||||||
});
|
|
||||||
|
|
||||||
// ===== 构建查询参数 =====
|
// ===== 构建查询参数 =====
|
||||||
const buildQueryParams = (): UserListQueryParams => {
|
const buildQueryParams = (): UserListQueryParams => {
|
||||||
const { page, pageSize } = pagination.params.value;
|
const { page, pageSize } = pagination.params.value;
|
||||||
@@ -104,15 +85,18 @@ export function useUserModel() {
|
|||||||
// ===== 计算属性 =====
|
// ===== 计算属性 =====
|
||||||
const hasFilter = computed(() => {
|
const hasFilter = computed(() => {
|
||||||
const f = filterForm;
|
const f = filterForm;
|
||||||
return !!debouncedText.value?.trim() || !!f.roleId || !!f.status;
|
return !!f.nickname?.trim() || !!f.phone?.trim() || !!f.status;
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== 事件处理 =====
|
// ===== 事件处理 =====
|
||||||
const handleSearch = () => fetchList();
|
const handleSearch = () => {
|
||||||
|
pagination.reset();
|
||||||
|
fetchList();
|
||||||
|
};
|
||||||
|
|
||||||
const handleReset = async () => {
|
const handleReset = async () => {
|
||||||
filterForm.text = '';
|
filterForm.nickname = '';
|
||||||
filterForm.roleId = '';
|
filterForm.phone = '';
|
||||||
filterForm.status = '';
|
filterForm.status = '';
|
||||||
pagination.reset();
|
pagination.reset();
|
||||||
await fetchList();
|
await fetchList();
|
||||||
@@ -120,20 +104,20 @@ export function useUserModel() {
|
|||||||
|
|
||||||
const handlePageChange = (page: number, pageSize: number) => {
|
const handlePageChange = (page: number, pageSize: number) => {
|
||||||
pagination.setCurrent(page);
|
pagination.setCurrent(page);
|
||||||
if (pageSize !== (pagination.pageSize as PageSizeRef)) {
|
if (pageSize !== pagination.pageSize) {
|
||||||
pagination.setPageSize(pageSize);
|
pagination.setPageSize(pageSize);
|
||||||
}
|
}
|
||||||
fetchList();
|
fetchList();
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===== 启用/停用用户 =====
|
// ===== 启用/禁用用户 =====
|
||||||
const handleToggleStatus = async (record: TournamentAdminUserVO) => {
|
const handleToggleStatus = async (record: TournamentAdminUserVO) => {
|
||||||
const isActive = record.status === '1';
|
const isActive = record.status === '1';
|
||||||
const actionText = isActive ? '停用' : '启用';
|
const actionText = isActive ? '禁用' : '启用';
|
||||||
|
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: `${actionText}确认`,
|
title: `${actionText}确认`,
|
||||||
content: `确认${actionText}用户"${record.realName}"吗?`,
|
content: `确认${actionText}用户"${record.nickname}"吗?`,
|
||||||
okText: '确认',
|
okText: '确认',
|
||||||
cancelText: '取消',
|
cancelText: '取消',
|
||||||
onOk: async () => {
|
onOk: async () => {
|
||||||
@@ -148,7 +132,6 @@ export function useUserModel() {
|
|||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(`用户${actionText}失败:`, e);
|
console.error(`用户${actionText}失败:`, e);
|
||||||
message.error(`用户${actionText}失败`);
|
|
||||||
} finally {
|
} finally {
|
||||||
setTogglingId('');
|
setTogglingId('');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
import { computed, reactive, toRef, Ref, nextTick } from 'vue';
|
import { computed, reactive, nextTick, h } from 'vue';
|
||||||
import {
|
import { StatusTag, type StatusTagTone } from '@/components';
|
||||||
useDebounce,
|
import { useThrottleFn, useRequest, usePagination, useEffect, syncPaginationTotal } from '@/hooks';
|
||||||
useThrottleFn,
|
|
||||||
useRequest,
|
|
||||||
usePagination,
|
|
||||||
useEffect,
|
|
||||||
syncPaginationTotal,
|
|
||||||
} from '@/hooks';
|
|
||||||
import { getPaymentFlowList, type PaymentFlowVO, type PaymentFlowQueryParams } from './services';
|
import { getPaymentFlowList, type PaymentFlowVO, type PaymentFlowQueryParams } from './services';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
@@ -20,11 +14,20 @@ export const PAYMENT_TYPE_OPTIONS = [
|
|||||||
{ value: '2', label: '取消报名' },
|
{ value: '2', label: '取消报名' },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const PAYMENT_TYPE_MAP: Record<number, string> = {
|
export const PAYMENT_TYPE_MAP: Record<number, { label: string; tone: StatusTagTone }> = {
|
||||||
1: '报名',
|
1: { label: '报名', tone: 'success' },
|
||||||
2: '取消报名',
|
2: { label: '取消报名', tone: 'default' },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 渲染交易类型 StatusTag */
|
||||||
|
export function renderPaymentType(type: number) {
|
||||||
|
const info = PAYMENT_TYPE_MAP[type] || {
|
||||||
|
label: String(type ?? '--'),
|
||||||
|
tone: 'default' as const,
|
||||||
|
};
|
||||||
|
return h(StatusTag, { label: info.label, tone: info.tone });
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Model
|
// Model
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -38,15 +41,6 @@ export function usePaymentsModel() {
|
|||||||
type: '',
|
type: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const { debouncedValue: debouncedNickname } = useDebounce(
|
|
||||||
toRef(filterForm, 'nickname') as Ref<string>,
|
|
||||||
{ delay: 300 },
|
|
||||||
);
|
|
||||||
const { debouncedValue: debouncedPhone } = useDebounce(
|
|
||||||
toRef(filterForm, 'phone') as Ref<string>,
|
|
||||||
{ delay: 300 },
|
|
||||||
);
|
|
||||||
|
|
||||||
// ===== 分页 =====
|
// ===== 分页 =====
|
||||||
const pagination = usePagination({ defaultCurrent: 1, defaultPageSize: 10 });
|
const pagination = usePagination({ defaultCurrent: 1, defaultPageSize: 10 });
|
||||||
|
|
||||||
@@ -61,8 +55,8 @@ export function usePaymentsModel() {
|
|||||||
if (start) params.createDateBegin = dayjs(start).format('YYYY-MM-DD');
|
if (start) params.createDateBegin = dayjs(start).format('YYYY-MM-DD');
|
||||||
if (end) params.createDateEnd = dayjs(end).format('YYYY-MM-DD');
|
if (end) params.createDateEnd = dayjs(end).format('YYYY-MM-DD');
|
||||||
}
|
}
|
||||||
if (debouncedNickname.value.trim()) params.nickname = debouncedNickname.value.trim();
|
if (filterForm.nickname.trim()) params.nickname = filterForm.nickname.trim();
|
||||||
if (debouncedPhone.value.trim()) params.phone = debouncedPhone.value.trim();
|
if (filterForm.phone.trim()) params.phone = filterForm.phone.trim();
|
||||||
if (filterForm.type) params.type = filterForm.type;
|
if (filterForm.type) params.type = filterForm.type;
|
||||||
return params;
|
return params;
|
||||||
};
|
};
|
||||||
@@ -93,14 +87,20 @@ export function usePaymentsModel() {
|
|||||||
|
|
||||||
// ===== 表格列(dataIndex 对齐 API) =====
|
// ===== 表格列(dataIndex 对齐 API) =====
|
||||||
const columns = [
|
const columns = [
|
||||||
{ title: '流水号', dataIndex: 'serialNo', key: 'serialNo', width: 160 },
|
{
|
||||||
|
title: '第三方流水号',
|
||||||
|
dataIndex: 'serialNo',
|
||||||
|
key: 'serialNo',
|
||||||
|
width: 160,
|
||||||
|
customRender: ({ text }: { text: string }) => text || '--',
|
||||||
|
},
|
||||||
{ title: '订单编号', dataIndex: 'orderNo', key: 'orderNo', width: 160 },
|
{ title: '订单编号', dataIndex: 'orderNo', key: 'orderNo', width: 160 },
|
||||||
{
|
{
|
||||||
title: '类型',
|
title: '类型',
|
||||||
dataIndex: 'type',
|
dataIndex: 'type',
|
||||||
key: 'type',
|
key: 'type',
|
||||||
width: 110,
|
width: 110,
|
||||||
customRender: ({ text }: { text: number }) => PAYMENT_TYPE_MAP[text] || text || '--',
|
customRender: ({ text }: { text: number }) => renderPaymentType(text),
|
||||||
},
|
},
|
||||||
{ title: '用户昵称', dataIndex: 'nickname', key: 'nickname', width: 120 },
|
{ title: '用户昵称', dataIndex: 'nickname', key: 'nickname', width: 120 },
|
||||||
{ title: '手机号', dataIndex: 'phone', key: 'phone', width: 140 },
|
{ title: '手机号', dataIndex: 'phone', key: 'phone', width: 140 },
|
||||||
@@ -118,15 +118,18 @@ export function usePaymentsModel() {
|
|||||||
const hasFilter = computed(() => {
|
const hasFilter = computed(() => {
|
||||||
return (
|
return (
|
||||||
filterForm.timeRange !== null ||
|
filterForm.timeRange !== null ||
|
||||||
debouncedNickname.value.trim() !== '' ||
|
filterForm.nickname.trim() !== '' ||
|
||||||
debouncedPhone.value.trim() !== '' ||
|
filterForm.phone.trim() !== '' ||
|
||||||
filterForm.type !== ''
|
filterForm.type !== ''
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== 方法 =====
|
// ===== 方法 =====
|
||||||
|
|
||||||
const handleSearch = () => fetchList();
|
const handleSearch = useThrottleFn(() => {
|
||||||
|
pagination.reset();
|
||||||
|
fetchList();
|
||||||
|
}, 300);
|
||||||
|
|
||||||
const handleReset = useThrottleFn(async () => {
|
const handleReset = useThrottleFn(async () => {
|
||||||
filterForm.timeRange = null;
|
filterForm.timeRange = null;
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'totalBalance',
|
key: 'totalBalance',
|
||||||
label: '平台余额(所有用户余额总和)',
|
label: '平台余额(所有用户余额总和 = 总收入 - 提现金额)',
|
||||||
value: s.totalBalance,
|
value: s.totalBalance,
|
||||||
bgColor: '#e6f4ff',
|
bgColor: '#e6f4ff',
|
||||||
accentColor: '#1677ff',
|
accentColor: '#1677ff',
|
||||||
@@ -224,7 +224,11 @@ export default defineComponent({
|
|||||||
<div class={styles.statValue} style={{ color: item.accentColor }}>
|
<div class={styles.statValue} style={{ color: item.accentColor }}>
|
||||||
¥{formatMoney(item.value)}
|
¥{formatMoney(item.value)}
|
||||||
</div>
|
</div>
|
||||||
{/* 趋势文字暂时隐藏 */}
|
{/* {item.key === 'totalBalance' && (
|
||||||
|
<div class={styles.statDesc} style={{ color: 'rgba(0, 0, 0, 0.45)' }}>
|
||||||
|
= 总收入 - 提现金额
|
||||||
|
</div>
|
||||||
|
)} */}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -126,7 +126,9 @@ export function useReportsModel() {
|
|||||||
// ===== 图表数据 =====
|
// ===== 图表数据 =====
|
||||||
const chartData = computed(() => {
|
const chartData = computed(() => {
|
||||||
const list = data.value?.monthList || [];
|
const list = data.value?.monthList || [];
|
||||||
return list.map((item) => ({
|
// 按月份升序排列,确保图表 X 轴时间顺序正确
|
||||||
|
const sorted = [...list].sort((a, b) => a.month.localeCompare(b.month));
|
||||||
|
return sorted.map((item) => ({
|
||||||
month: dayjs(item.month).format('MM月'),
|
month: dayjs(item.month).format('MM月'),
|
||||||
order: toNumber(item.orderAmount),
|
order: toNumber(item.orderAmount),
|
||||||
refund: toNumber(item.refundAmount),
|
refund: toNumber(item.refundAmount),
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
TRANSACTION_STATUS_OPTIONS,
|
TRANSACTION_STATUS_OPTIONS,
|
||||||
TRANSACTION_TYPE_MAP,
|
TRANSACTION_TYPE_MAP,
|
||||||
TRANSACTION_STATUS_LABEL_MAP,
|
TRANSACTION_STATUS_LABEL_MAP,
|
||||||
|
renderTransactionStatus,
|
||||||
} from '../model/useWalletModel';
|
} from '../model/useWalletModel';
|
||||||
import styles from './WalletDetailModal.module.less';
|
import styles from './WalletDetailModal.module.less';
|
||||||
|
|
||||||
@@ -124,13 +125,7 @@ export default defineComponent({
|
|||||||
customRender: ({ text }: { text: string }) => {
|
customRender: ({ text }: { text: string }) => {
|
||||||
const num = Number(text) || 0;
|
const num = Number(text) || 0;
|
||||||
const color = num > 0 ? '#52c41a' : num < 0 ? '#ff4d4f' : 'rgba(0,0,0,0.85)';
|
const color = num > 0 ? '#52c41a' : num < 0 ? '#ff4d4f' : 'rgba(0,0,0,0.85)';
|
||||||
const sign = num > 0 ? '+' : '';
|
return <span style={{ color, fontWeight: 500 }}>{text}</span>;
|
||||||
return (
|
|
||||||
<span style={{ color, fontWeight: 500 }}>
|
|
||||||
{sign}
|
|
||||||
{text}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -157,6 +152,13 @@ export default defineComponent({
|
|||||||
align: 'left' as const,
|
align: 'left' as const,
|
||||||
customRender: ({ text }: { text: string }) => `¥${text || '0.00'}`,
|
customRender: ({ text }: { text: string }) => `¥${text || '0.00'}`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
dataIndex: 'status',
|
||||||
|
key: 'status',
|
||||||
|
width: 90,
|
||||||
|
customRender: ({ text }: { text: number }) => renderTransactionStatus(text),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '冻结时间',
|
title: '冻结时间',
|
||||||
dataIndex: 'freezeTime',
|
dataIndex: 'freezeTime',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import { Button, Input, Table, Form, Space, Pagination } from 'ant-design-vue';
|
import { Button, Input, InputNumber, Table, Form, Space, Pagination } from 'ant-design-vue';
|
||||||
import { WalletOutlined, LockOutlined, RiseOutlined } from '@ant-design/icons-vue';
|
import { WalletOutlined, LockOutlined, RiseOutlined } from '@ant-design/icons-vue';
|
||||||
import { useWalletModel } from './model/useWalletModel';
|
import { useWalletModel } from './model/useWalletModel';
|
||||||
import { useContainerSize } from '@/hooks';
|
import { useContainerSize } from '@/hooks';
|
||||||
@@ -156,13 +156,18 @@ export default defineComponent({
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="余额筛选">
|
<Form.Item label="余额筛选">
|
||||||
<Space.Compact>
|
<Space.Compact>
|
||||||
<Input
|
<InputNumber
|
||||||
placeholder="请输入"
|
placeholder="请输入"
|
||||||
style={{ width: '130px' }}
|
style={{ width: '140px' }}
|
||||||
value={filterForm.minBalance}
|
value={filterForm.minBalance || undefined}
|
||||||
onUpdate:value={(val: any) => (filterForm.minBalance = val ?? '')}
|
precision={2}
|
||||||
|
step={0.01}
|
||||||
|
min={0}
|
||||||
|
controls={false}
|
||||||
|
onUpdate:value={(val: any) =>
|
||||||
|
(filterForm.minBalance = val != null ? String(val) : '')
|
||||||
|
}
|
||||||
onPressEnter={handleSearch}
|
onPressEnter={handleSearch}
|
||||||
allowClear
|
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
style={{
|
style={{
|
||||||
@@ -175,13 +180,17 @@ export default defineComponent({
|
|||||||
value="至"
|
value="至"
|
||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
<Input
|
<InputNumber
|
||||||
placeholder="请输入"
|
placeholder="请输入"
|
||||||
style={{ width: '130px' }}
|
style={{ width: '140px' }}
|
||||||
value={filterForm.maxBalance}
|
value={filterForm.maxBalance || undefined}
|
||||||
onUpdate:value={(val: any) => (filterForm.maxBalance = val ?? '')}
|
precision={0}
|
||||||
|
min={0}
|
||||||
|
controls={false}
|
||||||
|
onUpdate:value={(val: any) =>
|
||||||
|
(filterForm.maxBalance = val != null ? String(val) : '')
|
||||||
|
}
|
||||||
onPressEnter={handleSearch}
|
onPressEnter={handleSearch}
|
||||||
allowClear
|
|
||||||
/>
|
/>
|
||||||
</Space.Compact>
|
</Space.Compact>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { computed, reactive, toRef, Ref, nextTick } from 'vue';
|
import { computed, reactive, nextTick, h } from 'vue';
|
||||||
|
import { StatusTag, type StatusTagTone } from '@/components';
|
||||||
import {
|
import {
|
||||||
useState,
|
useState,
|
||||||
useDebounce,
|
|
||||||
useThrottleFn,
|
useThrottleFn,
|
||||||
useRequest,
|
useRequest,
|
||||||
usePagination,
|
usePagination,
|
||||||
@@ -37,13 +37,29 @@ export const TRANSACTION_TYPE_MAP: Record<number, string> = {
|
|||||||
3: '取消报名退款',
|
3: '取消报名退款',
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 交易状态文案映射(弹窗内表格渲染用) */
|
/** 交易状态文案映射(筛选下拉用,对应请求参数,值与接口返回一致) */
|
||||||
export const TRANSACTION_STATUS_LABEL_MAP: Record<number, string> = {
|
export const TRANSACTION_STATUS_LABEL_MAP: Record<number, string> = {
|
||||||
1: '已解冻',
|
1: '已解冻',
|
||||||
2: '冻结中',
|
2: '冻结中',
|
||||||
3: '正常',
|
3: '正常',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 交易状态色调映射(表格 StatusTag 显示用,对应接口返回的 status 值:1=已解冻,2=冻结中,3=正常) */
|
||||||
|
const TRANSACTION_STATUS_MAP: Record<number, { label: string; tone: StatusTagTone }> = {
|
||||||
|
1: { label: '已解冻', tone: 'success' },
|
||||||
|
2: { label: '冻结中', tone: 'warning' },
|
||||||
|
3: { label: '正常', tone: 'default' },
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 渲染交易状态 StatusTag(表格列用) */
|
||||||
|
export function renderTransactionStatus(status: number) {
|
||||||
|
const info = TRANSACTION_STATUS_MAP[status] || {
|
||||||
|
label: String(status ?? '--'),
|
||||||
|
tone: 'default' as const,
|
||||||
|
};
|
||||||
|
return h(StatusTag, { label: info.label, tone: info.tone });
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Model
|
// Model
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -57,15 +73,6 @@ export function useWalletModel() {
|
|||||||
maxBalance: '',
|
maxBalance: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const { debouncedValue: debouncedNickname } = useDebounce(
|
|
||||||
toRef(filterForm, 'nickname') as Ref<string>,
|
|
||||||
{ delay: 300 },
|
|
||||||
);
|
|
||||||
const { debouncedValue: debouncedPhone } = useDebounce(
|
|
||||||
toRef(filterForm, 'phone') as Ref<string>,
|
|
||||||
{ delay: 300 },
|
|
||||||
);
|
|
||||||
|
|
||||||
// ===== 分页 =====
|
// ===== 分页 =====
|
||||||
const pagination = usePagination({ defaultCurrent: 1, defaultPageSize: 10 });
|
const pagination = usePagination({ defaultCurrent: 1, defaultPageSize: 10 });
|
||||||
|
|
||||||
@@ -83,8 +90,8 @@ export function useWalletModel() {
|
|||||||
page: String((pagination as any).current.value),
|
page: String((pagination as any).current.value),
|
||||||
limit: String((pagination as any).pageSize.value),
|
limit: String((pagination as any).pageSize.value),
|
||||||
};
|
};
|
||||||
if (debouncedNickname.value.trim()) params.nickname = debouncedNickname.value.trim();
|
if (filterForm.nickname.trim()) params.nickname = filterForm.nickname.trim();
|
||||||
if (debouncedPhone.value.trim()) params.phone = debouncedPhone.value.trim();
|
if (filterForm.phone.trim()) params.phone = filterForm.phone.trim();
|
||||||
if (filterForm.minBalance) params.amountBegin = filterForm.minBalance;
|
if (filterForm.minBalance) params.amountBegin = filterForm.minBalance;
|
||||||
if (filterForm.maxBalance) params.amountEnd = filterForm.maxBalance;
|
if (filterForm.maxBalance) params.amountEnd = filterForm.maxBalance;
|
||||||
return params;
|
return params;
|
||||||
@@ -150,8 +157,8 @@ export function useWalletModel() {
|
|||||||
// ===== 计算属性 =====
|
// ===== 计算属性 =====
|
||||||
const hasFilter = computed(() => {
|
const hasFilter = computed(() => {
|
||||||
return (
|
return (
|
||||||
debouncedNickname.value.trim() !== '' ||
|
filterForm.nickname.trim() !== '' ||
|
||||||
debouncedPhone.value.trim() !== '' ||
|
filterForm.phone.trim() !== '' ||
|
||||||
filterForm.minBalance !== '' ||
|
filterForm.minBalance !== '' ||
|
||||||
filterForm.maxBalance !== ''
|
filterForm.maxBalance !== ''
|
||||||
);
|
);
|
||||||
@@ -159,7 +166,10 @@ export function useWalletModel() {
|
|||||||
|
|
||||||
// ===== 方法 =====
|
// ===== 方法 =====
|
||||||
|
|
||||||
const handleSearch = () => fetchList();
|
const handleSearch = useThrottleFn(() => {
|
||||||
|
pagination.reset();
|
||||||
|
fetchList();
|
||||||
|
}, 300);
|
||||||
|
|
||||||
const handleReset = useThrottleFn(async () => {
|
const handleReset = useThrottleFn(async () => {
|
||||||
filterForm.nickname = '';
|
filterForm.nickname = '';
|
||||||
|
|||||||
@@ -96,16 +96,12 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
color: rgba(0, 0, 0, 0.85);
|
color: rgba(0, 0, 0, 0.85);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.descValueBold {
|
|
||||||
font-weight: 600;
|
|
||||||
color: rgba(0, 0, 0, 0.85);
|
|
||||||
}
|
|
||||||
|
|
||||||
.auditImgList {
|
.auditImgList {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
@@ -147,9 +143,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ===== 上传 =====
|
// ===== 上传 =====
|
||||||
.uploader {
|
.uploadWrapper {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploader {
|
||||||
:global {
|
:global {
|
||||||
.ant-upload-list-picture-card-container {
|
.ant-upload-list-picture-card-container {
|
||||||
width: 96px;
|
width: 96px;
|
||||||
@@ -163,6 +164,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pasteTip {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #e6f7ff;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #1677ff;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.uploadTrigger {
|
.uploadTrigger {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { defineComponent, reactive, ref } from 'vue';
|
import { defineComponent, reactive, ref, onMounted, onUnmounted } from 'vue';
|
||||||
import { useEffect } from '@/hooks';
|
import { useEffect } from '@/hooks';
|
||||||
import { Modal, Radio, Input, Button, Upload, Image, Spin, message } from 'ant-design-vue';
|
import { Modal, Radio, Input, Button, Upload, Image, Spin, message } from 'ant-design-vue';
|
||||||
import type { UploadFile } from 'ant-design-vue';
|
import type { UploadFile } from 'ant-design-vue';
|
||||||
import { PlusOutlined } from '@ant-design/icons-vue';
|
import { PlusOutlined, CopyOutlined } from '@ant-design/icons-vue';
|
||||||
import { uploadFile, OssUploadType } from '@/utils/oss';
|
import { uploadFile, OssUploadType } from '@/utils/oss';
|
||||||
import { getWithdrawInfo, postWithdrawAudit, type WithdrawInfoVO } from '../model/services';
|
import { getWithdrawInfo, postWithdrawAudit, type WithdrawInfoVO } from '../model/services';
|
||||||
import { getAuditStatusText, getPayStatusText } from '../model/useWithdrawModel';
|
import { getAuditStatusText, getPayStatusText } from '../model/useWithdrawModel';
|
||||||
@@ -41,6 +41,34 @@ export default defineComponent({
|
|||||||
const auditForm = reactive({ pass: true, remark: '' });
|
const auditForm = reactive({ pass: true, remark: '' });
|
||||||
const imageList = ref<UploadFile[]>([]);
|
const imageList = ref<UploadFile[]>([]);
|
||||||
const submitting = ref(false);
|
const submitting = ref(false);
|
||||||
|
const uploadAreaRef = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
// ===== 粘贴图片上传 =====
|
||||||
|
const handlePaste = async (e: ClipboardEvent) => {
|
||||||
|
if (!props.visible || !uploadAreaRef.value) return;
|
||||||
|
|
||||||
|
const items = e.clipboardData?.items;
|
||||||
|
if (!items) return;
|
||||||
|
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
const item = items[i];
|
||||||
|
if (item.type.indexOf('image') !== -1) {
|
||||||
|
e.preventDefault();
|
||||||
|
const file = item.getAsFile();
|
||||||
|
if (file) {
|
||||||
|
await beforeUpload(file as unknown as UploadFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('paste', handlePaste);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('paste', handlePaste);
|
||||||
|
});
|
||||||
|
|
||||||
// ===== 拉取详情 =====
|
// ===== 拉取详情 =====
|
||||||
const fetchDetail = async (id: number) => {
|
const fetchDetail = async (id: number) => {
|
||||||
@@ -150,17 +178,17 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderItem = (label: string, value: any, isBold = false) => (
|
const renderItem = (label: string, value: any) => (
|
||||||
<div class={styles.descItem}>
|
<div class={styles.descItem}>
|
||||||
<span class={styles.descLabel}>{label}</span>
|
<span class={styles.descLabel}>{label}</span>
|
||||||
<span class={[styles.descValue, isBold ? styles.descValueBold : '']}>{value || '--'}</span>
|
<span class={styles.descValue}>{value || '--'}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
/** 渲染内容区(加载完成后) */
|
/** 渲染内容区(加载完成后) */
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
const d = detail.value!;
|
const d = detail.value!;
|
||||||
const bankCardTypeLabel = d.bankCardType === 1 ? '个人' : d.bankCardType || '--';
|
const bankCardTypeLabel = d.bankCardType == 1 ? '个人' : d.bankCardType || '--';
|
||||||
const isPending = d.auditStatus == 0;
|
const isPending = d.auditStatus == 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -175,12 +203,12 @@ export default defineComponent({
|
|||||||
{renderItem('申请时间:', d.createDate)}
|
{renderItem('申请时间:', d.createDate)}
|
||||||
{renderItem('打款类型:', d.withdrawType)}
|
{renderItem('打款类型:', d.withdrawType)}
|
||||||
<div style="display: contents;" />
|
<div style="display: contents;" />
|
||||||
{renderItem('提现金额:', `¥${d.withdrawAmount}`, true)}
|
{renderItem('提现金额:', `¥${d.withdrawAmount}`)}
|
||||||
{renderItem('费率:', `${(Number(d.feeRate) * 100).toFixed(2)}%`)}
|
{renderItem('费率:', `${(Number(d.feeRate) * 100).toFixed(2)}%`)}
|
||||||
{renderItem('手续费:', `¥${d.feeAmount}`)}
|
{renderItem('手续费:', `¥${d.feeAmount}`)}
|
||||||
{renderItem('到账金额:', `¥${d.receivedAmount}`, true)}
|
{renderItem('到账金额:', `¥${d.receivedAmount}`)}
|
||||||
{renderItem('审核状态:', getAuditStatusText(d.auditStatus), true)}
|
{renderItem('审核状态:', getAuditStatusText(d.auditStatus))}
|
||||||
{renderItem('列账状态:', getPayStatusText(d.payStatus))}
|
{renderItem('到账状态:', getPayStatusText(d.payStatus))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -239,21 +267,27 @@ export default defineComponent({
|
|||||||
{auditForm.pass && <span class={styles.required}>*</span>}
|
{auditForm.pass && <span class={styles.required}>*</span>}
|
||||||
审核图片
|
审核图片
|
||||||
</span>
|
</span>
|
||||||
<Upload
|
<div ref={uploadAreaRef} class={styles.uploadWrapper}>
|
||||||
listType="picture-card"
|
<Upload
|
||||||
fileList={imageList.value}
|
listType="picture-card"
|
||||||
beforeUpload={beforeUpload}
|
fileList={imageList.value}
|
||||||
onRemove={handleRemove}
|
beforeUpload={beforeUpload}
|
||||||
accept="image/*"
|
onRemove={handleRemove}
|
||||||
class={styles.uploader}
|
accept="image/*"
|
||||||
>
|
class={styles.uploader}
|
||||||
{imageList.value.length >= MAX_IMAGE_COUNT ? null : (
|
>
|
||||||
<div class={styles.uploadTrigger}>
|
{imageList.value.length >= MAX_IMAGE_COUNT ? null : (
|
||||||
<PlusOutlined />
|
<div class={styles.uploadTrigger}>
|
||||||
<div class={styles.uploadText}>请选择图片</div>
|
<PlusOutlined />
|
||||||
</div>
|
<div class={styles.uploadText}>选择或粘贴图片</div>
|
||||||
)}
|
</div>
|
||||||
</Upload>
|
)}
|
||||||
|
</Upload>
|
||||||
|
<div class={styles.pasteTip}>
|
||||||
|
<CopyOutlined />
|
||||||
|
<span>支持 Ctrl+V 粘贴图片</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class={styles.footer}>
|
<div class={styles.footer}>
|
||||||
<Button type="primary" onClick={handleSubmit} loading={submitting.value}>
|
<Button type="primary" onClick={handleSubmit} loading={submitting.value}>
|
||||||
@@ -267,6 +301,7 @@ export default defineComponent({
|
|||||||
<div class={styles.infoColumn}>
|
<div class={styles.infoColumn}>
|
||||||
{renderItem('审核时间:', d.auditDate)}
|
{renderItem('审核时间:', d.auditDate)}
|
||||||
{renderItem('审核人:', d.auditNickname)}
|
{renderItem('审核人:', d.auditNickname)}
|
||||||
|
{d.checkMsg && renderItem('审核内容:', d.checkMsg)}
|
||||||
<div class={styles.descItem}>
|
<div class={styles.descItem}>
|
||||||
<span class={styles.descLabel}>审核图片:</span>
|
<span class={styles.descLabel}>审核图片:</span>
|
||||||
{d.auditImgList?.length ? (
|
{d.auditImgList?.length ? (
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { computed, reactive, toRef, Ref, h, nextTick } from 'vue';
|
import { computed, reactive, h, nextTick } from 'vue';
|
||||||
import { StatusTag, type StatusTagTone } from '@/components';
|
import { StatusTag, type StatusTagTone } from '@/components';
|
||||||
import {
|
import {
|
||||||
useState,
|
useState,
|
||||||
useDebounce,
|
|
||||||
useThrottleFn,
|
useThrottleFn,
|
||||||
useRequest,
|
useRequest,
|
||||||
usePagination,
|
usePagination,
|
||||||
@@ -87,20 +86,6 @@ export function useWithdrawModel() {
|
|||||||
payStatus: '',
|
payStatus: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
// 可搜索字段防抖
|
|
||||||
const { debouncedValue: debouncedNickname } = useDebounce(
|
|
||||||
toRef(filterForm, 'nickname') as Ref<string>,
|
|
||||||
{ delay: 300 },
|
|
||||||
);
|
|
||||||
const { debouncedValue: debouncedPhone } = useDebounce(
|
|
||||||
toRef(filterForm, 'phone') as Ref<string>,
|
|
||||||
{ delay: 300 },
|
|
||||||
);
|
|
||||||
const { debouncedValue: debouncedRealName } = useDebounce(
|
|
||||||
toRef(filterForm, 'realName') as Ref<string>,
|
|
||||||
{ delay: 300 },
|
|
||||||
);
|
|
||||||
|
|
||||||
// ===== 分页 =====
|
// ===== 分页 =====
|
||||||
const pagination = usePagination({ defaultCurrent: 1, defaultPageSize: 10 });
|
const pagination = usePagination({ defaultCurrent: 1, defaultPageSize: 10 });
|
||||||
|
|
||||||
@@ -110,9 +95,9 @@ export function useWithdrawModel() {
|
|||||||
page: String((pagination as any).current.value),
|
page: String((pagination as any).current.value),
|
||||||
limit: String((pagination as any).pageSize.value),
|
limit: String((pagination as any).pageSize.value),
|
||||||
};
|
};
|
||||||
if (debouncedNickname.value.trim()) params.nickname = debouncedNickname.value.trim();
|
if (filterForm.nickname.trim()) params.nickname = filterForm.nickname.trim();
|
||||||
if (debouncedPhone.value.trim()) params.phone = debouncedPhone.value.trim();
|
if (filterForm.phone.trim()) params.phone = filterForm.phone.trim();
|
||||||
if (debouncedRealName.value.trim()) params.realName = debouncedRealName.value.trim();
|
if (filterForm.realName.trim()) params.realName = filterForm.realName.trim();
|
||||||
if (filterForm.auditStatus) params.auditStatus = filterForm.auditStatus;
|
if (filterForm.auditStatus) params.auditStatus = filterForm.auditStatus;
|
||||||
if (filterForm.payStatus) params.payStatus = filterForm.payStatus;
|
if (filterForm.payStatus) params.payStatus = filterForm.payStatus;
|
||||||
return params;
|
return params;
|
||||||
@@ -162,16 +147,16 @@ export function useWithdrawModel() {
|
|||||||
},
|
},
|
||||||
{ title: '审核状态', dataIndex: 'auditStatus', key: 'auditStatus', width: 110 },
|
{ title: '审核状态', dataIndex: 'auditStatus', key: 'auditStatus', width: 110 },
|
||||||
{ title: '到账状态', dataIndex: 'payStatus', key: 'payStatus', width: 110 },
|
{ title: '到账状态', dataIndex: 'payStatus', key: 'payStatus', width: 110 },
|
||||||
{ title: '流水单号', dataIndex: 'serialNumber', key: 'serialNumber', width: 180 },
|
{ title: '第三方单号', dataIndex: 'serialNumber', key: 'serialNumber', width: 180 },
|
||||||
{ title: '申请时间', dataIndex: 'createDate', key: 'createDate', width: 180 },
|
{ title: '申请时间', dataIndex: 'createDate', key: 'createDate', width: 180 },
|
||||||
];
|
];
|
||||||
|
|
||||||
// ===== 计算属性 =====
|
// ===== 计算属性 =====
|
||||||
const hasFilter = computed(() => {
|
const hasFilter = computed(() => {
|
||||||
return (
|
return (
|
||||||
debouncedNickname.value.trim() !== '' ||
|
filterForm.nickname.trim() !== '' ||
|
||||||
debouncedPhone.value.trim() !== '' ||
|
filterForm.phone.trim() !== '' ||
|
||||||
debouncedRealName.value.trim() !== '' ||
|
filterForm.realName.trim() !== '' ||
|
||||||
filterForm.auditStatus !== '' ||
|
filterForm.auditStatus !== '' ||
|
||||||
filterForm.payStatus !== ''
|
filterForm.payStatus !== ''
|
||||||
);
|
);
|
||||||
@@ -179,7 +164,10 @@ export function useWithdrawModel() {
|
|||||||
|
|
||||||
// ===== 方法 =====
|
// ===== 方法 =====
|
||||||
|
|
||||||
const handleSearch = () => fetchList();
|
const handleSearch = useThrottleFn(() => {
|
||||||
|
pagination.reset();
|
||||||
|
fetchList();
|
||||||
|
}, 300);
|
||||||
|
|
||||||
/** 重置(重置后自动查询) */
|
/** 重置(重置后自动查询) */
|
||||||
const handleReset = useThrottleFn(async () => {
|
const handleReset = useThrottleFn(async () => {
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ export default defineComponent({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class={styles.brandFooter}>六个羽友赛事运营平台 ©{new Date().getFullYear()}</div>
|
{/* <div class={styles.brandFooter}>六个羽友赛事运营平台 ©{new Date().getFullYear()}</div> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -186,7 +186,7 @@ export default defineComponent({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
<div class={styles.loginFooter}>六个羽友赛事运营平台 ©{new Date().getFullYear()}</div>
|
{/* <div class={styles.loginFooter}>六个羽友赛事运营平台 ©{new Date().getFullYear()}</div> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { computed, reactive, toRef, Ref, nextTick } from 'vue';
|
import { computed, reactive, nextTick } from 'vue';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useDebounce, useThrottleFn } from '@/hooks';
|
import { useThrottleFn } from '@/hooks';
|
||||||
import { useRequest } from '@/hooks/useRequest';
|
import { useRequest } from '@/hooks/useRequest';
|
||||||
import { useEffect } from '@/hooks/useEffect';
|
import { useEffect } from '@/hooks/useEffect';
|
||||||
import { usePagination, syncPaginationTotal } from '@/hooks/usePagination';
|
import { usePagination, syncPaginationTotal } from '@/hooks/usePagination';
|
||||||
@@ -23,6 +23,7 @@ export const ACTION_TYPE_OPTIONS = [
|
|||||||
{ value: '2', label: '上架赛事' },
|
{ value: '2', label: '上架赛事' },
|
||||||
{ value: '3', label: '禁用用户' },
|
{ value: '3', label: '禁用用户' },
|
||||||
{ value: '4', label: '启用用户' },
|
{ value: '4', label: '启用用户' },
|
||||||
|
{ value: '7', label: '系统参数' },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
/** 操作类型文案映射 */
|
/** 操作类型文案映射 */
|
||||||
@@ -31,6 +32,7 @@ export const ACTION_TYPE_MAP: Record<number, string> = {
|
|||||||
2: '上架赛事',
|
2: '上架赛事',
|
||||||
3: '禁用用户',
|
3: '禁用用户',
|
||||||
4: '启用用户',
|
4: '启用用户',
|
||||||
|
7: '系统参数',
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -48,12 +50,6 @@ export function useLogModel() {
|
|||||||
searchOperator: '',
|
searchOperator: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
// 操作人字段防抖 300ms
|
|
||||||
const { debouncedValue: debouncedOperator } = useDebounce(
|
|
||||||
toRef(filterForm, 'searchOperator') as Ref<string>,
|
|
||||||
{ delay: 300 },
|
|
||||||
);
|
|
||||||
|
|
||||||
// ===== 分页 =====
|
// ===== 分页 =====
|
||||||
const pagination = usePagination({ defaultCurrent: 1, defaultPageSize: 10 });
|
const pagination = usePagination({ defaultCurrent: 1, defaultPageSize: 10 });
|
||||||
|
|
||||||
@@ -76,9 +72,9 @@ export function useLogModel() {
|
|||||||
params.type = filterForm.actionType;
|
params.type = filterForm.actionType;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 操作人昵称(防抖后的值)
|
// 操作人昵称
|
||||||
if (debouncedOperator.value.trim()) {
|
if (filterForm.searchOperator.trim()) {
|
||||||
params.nickname = debouncedOperator.value.trim();
|
params.nickname = filterForm.searchOperator.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
return params;
|
return params;
|
||||||
@@ -116,13 +112,16 @@ export function useLogModel() {
|
|||||||
return (
|
return (
|
||||||
filterForm.actionTimeRange !== null ||
|
filterForm.actionTimeRange !== null ||
|
||||||
filterForm.actionType !== '' ||
|
filterForm.actionType !== '' ||
|
||||||
debouncedOperator.value.trim() !== ''
|
filterForm.searchOperator.trim() !== ''
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== 方法 =====
|
// ===== 方法 =====
|
||||||
|
|
||||||
const handleSearch = () => fetchList();
|
const handleSearch = useThrottleFn(() => {
|
||||||
|
pagination.reset();
|
||||||
|
fetchList();
|
||||||
|
}, 300);
|
||||||
|
|
||||||
/** 重置(重置后自动查询) */
|
/** 重置(重置后自动查询) */
|
||||||
const handleReset = useThrottleFn(async () => {
|
const handleReset = useThrottleFn(async () => {
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
.paramFormModalMain {
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import { defineComponent, ref, reactive, nextTick } from 'vue';
|
||||||
|
import { useEffect } from '@/hooks';
|
||||||
|
import { Modal, Form, Input, Button } from 'ant-design-vue';
|
||||||
|
import { paramCodeRules, paramValueRules, remarkRules } from '../model/form';
|
||||||
|
import type { SysParamsDTO } from '../model/services';
|
||||||
|
import styles from './ParamFormModal.module.less';
|
||||||
|
|
||||||
|
const { TextArea } = Input;
|
||||||
|
|
||||||
|
interface ParamFormModalProps {
|
||||||
|
visible: boolean;
|
||||||
|
record: SysParamsDTO | null;
|
||||||
|
submitting?: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSave: (formData: { paramCode: string; paramValue: string; remark: string }) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 默认表单数据 */
|
||||||
|
const getDefaultForm = () => ({
|
||||||
|
paramCode: '',
|
||||||
|
paramValue: '',
|
||||||
|
remark: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'ParamFormModal',
|
||||||
|
props: {
|
||||||
|
visible: { type: Boolean, default: false },
|
||||||
|
record: { type: Object, default: null },
|
||||||
|
submitting: { type: Boolean, default: false },
|
||||||
|
onClose: { type: Function, required: true },
|
||||||
|
onSave: { type: Function, required: true },
|
||||||
|
},
|
||||||
|
setup(props: ParamFormModalProps) {
|
||||||
|
const formRef = ref<any>();
|
||||||
|
const formData = reactive(getDefaultForm());
|
||||||
|
|
||||||
|
/** 根据 record 初始化表单 */
|
||||||
|
const initFormFromRecord = (record: SysParamsDTO | null) => {
|
||||||
|
if (record) {
|
||||||
|
Object.assign(formData, {
|
||||||
|
paramCode: record.paramCode || '',
|
||||||
|
paramValue: record.paramValue || '',
|
||||||
|
remark: record.remark || '',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Object.assign(formData, getDefaultForm());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 监听 visible 变化重置表单 */
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.visible) {
|
||||||
|
initFormFromRecord(props.record);
|
||||||
|
nextTick().then(() => formRef.value?.clearValidate());
|
||||||
|
}
|
||||||
|
}, [() => props.visible]);
|
||||||
|
|
||||||
|
/** 提交 */
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
await formRef.value?.validate();
|
||||||
|
|
||||||
|
props.onSave({
|
||||||
|
paramCode: formData.paramCode,
|
||||||
|
paramValue: formData.paramValue,
|
||||||
|
remark: formData.remark,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// 校验未通过,Form 已展示错误,无需处理
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 关闭 */
|
||||||
|
const handleClose = () => {
|
||||||
|
props.onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
const isEdit = !!props.record;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={isEdit ? '编辑配置' : '新增配置'}
|
||||||
|
visible={props.visible}
|
||||||
|
onCancel={handleClose}
|
||||||
|
width={520}
|
||||||
|
destroyOnClose
|
||||||
|
footer={null}
|
||||||
|
centered
|
||||||
|
wrapClassName={styles.paramFormModalMain}
|
||||||
|
>
|
||||||
|
<Form ref={formRef} layout="vertical" model={formData} requiredMark>
|
||||||
|
<Form.Item label="参数编码" name="paramCode" rules={paramCodeRules}>
|
||||||
|
<Input
|
||||||
|
value={formData.paramCode}
|
||||||
|
onUpdate:value={(val: any) => {
|
||||||
|
formData.paramCode = val?.target?.value ?? val ?? '';
|
||||||
|
}}
|
||||||
|
placeholder="请输入参数编码"
|
||||||
|
allowClear
|
||||||
|
readonly={isEdit}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item label="参数值" name="paramValue" rules={paramValueRules}>
|
||||||
|
<Input
|
||||||
|
value={formData.paramValue}
|
||||||
|
onUpdate:value={(val: any) => {
|
||||||
|
formData.paramValue = val?.target?.value ?? val ?? '';
|
||||||
|
}}
|
||||||
|
placeholder="请输入参数值"
|
||||||
|
allowClear
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item label="备注说明" name="remark" rules={remarkRules}>
|
||||||
|
<TextArea
|
||||||
|
value={formData.remark}
|
||||||
|
onUpdate:value={(val: any) => {
|
||||||
|
formData.remark = val?.target?.value ?? val ?? '';
|
||||||
|
}}
|
||||||
|
placeholder="请输入备注说明(选填,最多200字)"
|
||||||
|
maxlength={200}
|
||||||
|
showCount
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
{/* 底部按钮 */}
|
||||||
|
<div class={styles.footer}>
|
||||||
|
<Button onClick={handleClose}>取消</Button>
|
||||||
|
<Button type="primary" loading={props.submitting} onClick={handleSubmit}>
|
||||||
|
确认保存
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
.cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
width: 100%;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
line-height: 1.5;
|
||||||
|
word-break: break-all;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
}
|
||||||
|
|
||||||
|
.view {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: #1677ff;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
line-height: 1.5;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
import { defineComponent, ref, nextTick } from 'vue';
|
||||||
|
import { Button, Input, Table, Form, Space, Modal, Pagination, message } from 'ant-design-vue';
|
||||||
|
import { useParamsModel } from './model/useParamsModel';
|
||||||
|
import { saveParam, updateParam } from './model/services';
|
||||||
|
import { useState, useThrottleFn, useContainerSize, useEffect } from '@/hooks';
|
||||||
|
import ParamFormModal from './components/ParamFormModal';
|
||||||
|
import pageStyles from '@/assets/styles/pageLayout.module.less';
|
||||||
|
import cellStyles from './index.module.less';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 参数值单元格:最多 2 行截断,溢出时显示"查看"按钮
|
||||||
|
*/
|
||||||
|
const ParamValueCell = defineComponent({
|
||||||
|
name: 'ParamValueCell',
|
||||||
|
props: { text: { type: String, required: true } },
|
||||||
|
setup(props) {
|
||||||
|
const textRef = ref<HTMLElement | null>(null);
|
||||||
|
const [overflow, setOverflow] = useState(false);
|
||||||
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const checkOverflow = () => {
|
||||||
|
const el = textRef.value;
|
||||||
|
if (el) setOverflow(el.scrollHeight > el.clientHeight);
|
||||||
|
};
|
||||||
|
|
||||||
|
let observer: ResizeObserver | null = null;
|
||||||
|
useEffect(() => {
|
||||||
|
if (!textRef.value) return;
|
||||||
|
nextTick(checkOverflow);
|
||||||
|
observer = new ResizeObserver(checkOverflow);
|
||||||
|
observer.observe(textRef.value);
|
||||||
|
return () => {
|
||||||
|
observer?.disconnect();
|
||||||
|
observer = null;
|
||||||
|
};
|
||||||
|
}, [() => textRef.value]);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
const raw = props.text;
|
||||||
|
return (
|
||||||
|
<div class={cellStyles.cell}>
|
||||||
|
<div ref={textRef} class={cellStyles.text}>
|
||||||
|
{raw}
|
||||||
|
</div>
|
||||||
|
{overflow.value ? (
|
||||||
|
<span class={cellStyles.view} onClick={() => setModalOpen(true)}>
|
||||||
|
查看
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
<Modal
|
||||||
|
visible={modalOpen.value}
|
||||||
|
title="参数值"
|
||||||
|
footer={null}
|
||||||
|
width={600}
|
||||||
|
centered
|
||||||
|
destroyOnClose
|
||||||
|
onCancel={() => setModalOpen(false)}
|
||||||
|
>
|
||||||
|
<div style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all', lineHeight: 1.8 }}>
|
||||||
|
{raw}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* bodyCell 渲染
|
||||||
|
*/
|
||||||
|
function renderBodyCell({
|
||||||
|
column,
|
||||||
|
text,
|
||||||
|
record,
|
||||||
|
onEdit,
|
||||||
|
}: {
|
||||||
|
column: any;
|
||||||
|
text: any;
|
||||||
|
record: any;
|
||||||
|
onEdit: (record: any) => void;
|
||||||
|
}) {
|
||||||
|
// 操作列
|
||||||
|
if (column.key === 'action') {
|
||||||
|
return (
|
||||||
|
<Space>
|
||||||
|
<Button type="link" size="small" onClick={() => onEdit(record)}>
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// 参数值列:两行截断 + 查看弹窗
|
||||||
|
if (column.dataIndex === 'paramValue') {
|
||||||
|
const raw = text || '';
|
||||||
|
return raw ? <ParamValueCell text={raw} /> : <span>--</span>;
|
||||||
|
}
|
||||||
|
// 备注列:空值显示 --
|
||||||
|
if (column.dataIndex === 'remark' && !text) {
|
||||||
|
return <span>--</span>;
|
||||||
|
}
|
||||||
|
return <span>{text ?? '--'}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统管理 - 配置管理
|
||||||
|
*/
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'SystemParams',
|
||||||
|
setup() {
|
||||||
|
const {
|
||||||
|
filterForm,
|
||||||
|
loading,
|
||||||
|
dataSource,
|
||||||
|
columns,
|
||||||
|
pagination,
|
||||||
|
handleSearch,
|
||||||
|
handleReset,
|
||||||
|
handlePageChange,
|
||||||
|
} = useParamsModel();
|
||||||
|
|
||||||
|
const { containerRef, height } = useContainerSize({ headerOffset: 55 });
|
||||||
|
|
||||||
|
// ===== 弹窗状态 =====
|
||||||
|
const [modalVisible, setModalVisible] = useState<boolean>(false);
|
||||||
|
const [editingRecord, setEditingRecord] = useState<any>(null);
|
||||||
|
const [formSubmitting, setFormSubmitting] = useState<boolean>(false);
|
||||||
|
|
||||||
|
// ===== 新增 =====
|
||||||
|
const handleAdd = () => {
|
||||||
|
setEditingRecord(null);
|
||||||
|
setModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== 编辑 =====
|
||||||
|
const handleEdit = (record: any) => {
|
||||||
|
setEditingRecord(record);
|
||||||
|
setModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== 关闭弹窗 =====
|
||||||
|
const handleCloseModal = () => {
|
||||||
|
setModalVisible(false);
|
||||||
|
setEditingRecord(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== 保存(新增/编辑) =====
|
||||||
|
const handleSave = useThrottleFn(async (formPayload: any) => {
|
||||||
|
if (formSubmitting.value) return;
|
||||||
|
setFormSubmitting(true);
|
||||||
|
try {
|
||||||
|
let res: any;
|
||||||
|
if (editingRecord.value) {
|
||||||
|
res = await updateParam({
|
||||||
|
id: editingRecord.value.id,
|
||||||
|
paramCode: formPayload.paramCode,
|
||||||
|
paramValue: formPayload.paramValue,
|
||||||
|
remark: formPayload.remark,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res = await saveParam({
|
||||||
|
paramCode: formPayload.paramCode,
|
||||||
|
paramValue: formPayload.paramValue,
|
||||||
|
remark: formPayload.remark,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (res.code == 200) {
|
||||||
|
message.success(editingRecord.value ? '编辑成功' : '新增成功');
|
||||||
|
handleCloseModal();
|
||||||
|
handleSearch();
|
||||||
|
} else {
|
||||||
|
message.error(res.msg || '操作失败');
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('保存配置失败:', error);
|
||||||
|
} finally {
|
||||||
|
setFormSubmitting(false);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
// ===== 表格最终列(含操作列) =====
|
||||||
|
const tableColumns = [
|
||||||
|
...columns,
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
width: 100,
|
||||||
|
fixed: 'right' as const,
|
||||||
|
align: 'center' as const,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return () => (
|
||||||
|
<div class={pageStyles.containerMain}>
|
||||||
|
{/* ===== 筛选区 ===== */}
|
||||||
|
<div class={pageStyles.filter}>
|
||||||
|
<Form layout="inline" model={filterForm}>
|
||||||
|
<Form.Item label="参数编码" name="paramCode">
|
||||||
|
<Input
|
||||||
|
value={filterForm.paramCode}
|
||||||
|
placeholder="请输入参数编码"
|
||||||
|
style={{ width: '200px' }}
|
||||||
|
allowClear
|
||||||
|
onUpdate:value={(val: any) =>
|
||||||
|
(filterForm.paramCode = val?.target?.value ?? val ?? '')
|
||||||
|
}
|
||||||
|
onPressEnter={handleSearch}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item>
|
||||||
|
<Space>
|
||||||
|
<Button onClick={handleReset}>重置</Button>
|
||||||
|
<Button type="primary" onClick={handleSearch} loading={loading.value}>
|
||||||
|
查询
|
||||||
|
</Button>
|
||||||
|
<Button type="primary" onClick={handleAdd}>
|
||||||
|
新增
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ===== 表格区 ===== */}
|
||||||
|
<div class={pageStyles.table}>
|
||||||
|
<div ref={containerRef} class={pageStyles.tableBody}>
|
||||||
|
<Table
|
||||||
|
columns={tableColumns}
|
||||||
|
dataSource={dataSource.value}
|
||||||
|
loading={loading.value}
|
||||||
|
scroll={{ x: 'max-content', y: height.value }}
|
||||||
|
pagination={false}
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
bodyCell: (args: any) =>
|
||||||
|
renderBodyCell({
|
||||||
|
...args,
|
||||||
|
onEdit: handleEdit,
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 独立分页 */}
|
||||||
|
<div class={pageStyles.pagination}>
|
||||||
|
<Pagination
|
||||||
|
current={(pagination as any).current.value}
|
||||||
|
pageSize={(pagination as any).pageSize.value}
|
||||||
|
total={(pagination as any).total.value}
|
||||||
|
showSizeChanger
|
||||||
|
showTotal={(total: number) => `共 ${total} 条`}
|
||||||
|
onChange={handlePageChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ===== 新增/编辑弹窗 ===== */}
|
||||||
|
{modalVisible.value && (
|
||||||
|
<ParamFormModal
|
||||||
|
visible={modalVisible.value}
|
||||||
|
record={editingRecord.value}
|
||||||
|
submitting={formSubmitting.value}
|
||||||
|
onClose={handleCloseModal}
|
||||||
|
onSave={handleSave}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* 参数配置 - 常量配置
|
||||||
|
*
|
||||||
|
* 集中放置该页面的下拉选项、字段映射、状态文案等常量
|
||||||
|
*/
|
||||||
|
import type { SelectOption } from '@/types';
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 字段映射(表格列与 API 字段对齐)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/** API 字段 → 表格列中文映射 */
|
||||||
|
export const FIELD_MAPPINGS: Record<string, string> = {
|
||||||
|
// TODO: 等后端接口确定后,根据实际字段补充
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 下拉选项
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/** 示例:状态筛选选项 */
|
||||||
|
export const STATUS_OPTIONS: SelectOption[] = [{ value: '', label: '全部' }];
|
||||||
|
|
||||||
|
/** 示例:每页条数选项 */
|
||||||
|
export const PAGE_SIZE_OPTIONS: SelectOption[] = [
|
||||||
|
{ value: 10, label: '10条/页' },
|
||||||
|
{ value: 20, label: '20条/页' },
|
||||||
|
{ value: 50, label: '50条/页' },
|
||||||
|
];
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* 参数配置 - 表单校验规则
|
||||||
|
*
|
||||||
|
* 集中管理新增/编辑弹窗的表单校验规则。
|
||||||
|
*/
|
||||||
|
import type { Rule } from 'ant-design-vue/es/form';
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 校验规则
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/** 参数编码:必填 */
|
||||||
|
export const paramCodeRules: Rule[] = [{ required: true, message: '请输入参数编码' }];
|
||||||
|
|
||||||
|
/** 参数值:必填 */
|
||||||
|
export const paramValueRules: Rule[] = [{ required: true, message: '请输入参数值' }];
|
||||||
|
|
||||||
|
/** 备注说明:选填,最多 200 字 */
|
||||||
|
export const remarkRules: Rule[] = [{ max: 200, message: '备注说明最多 200 个字' }];
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
/**
|
||||||
|
* 参数配置 - 接口服务
|
||||||
|
*
|
||||||
|
* 该页面使用的所有接口集中管理,供 useModel Hook 调用。
|
||||||
|
*/
|
||||||
|
import { get, post } from '@/utils/request';
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 类型定义
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/** 配置项 VO */
|
||||||
|
export interface SysParamsDTO {
|
||||||
|
id: string;
|
||||||
|
paramCode: string;
|
||||||
|
paramValue: string;
|
||||||
|
remark: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 分页查询参数 */
|
||||||
|
export interface ParamQueryParams {
|
||||||
|
page: string;
|
||||||
|
limit: string;
|
||||||
|
paramCode?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 新增参数 */
|
||||||
|
export interface ParamSaveParams {
|
||||||
|
paramCode: string;
|
||||||
|
paramValue: string;
|
||||||
|
remark?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 编辑参数 */
|
||||||
|
export interface ParamUpdateParams {
|
||||||
|
id: string;
|
||||||
|
paramCode: string;
|
||||||
|
paramValue: string;
|
||||||
|
remark?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 分页数据结构 */
|
||||||
|
export interface PageData<T> {
|
||||||
|
total: number;
|
||||||
|
list: T[];
|
||||||
|
exData?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 接口通用返回结构 */
|
||||||
|
export interface ApiResult<T> {
|
||||||
|
code: number;
|
||||||
|
msg: string;
|
||||||
|
data: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// URL 常量
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
const PARAM_PAGE = '/sys/params/page';
|
||||||
|
const PARAM_SAVE = '/sys/params/save';
|
||||||
|
const PARAM_UPDATE = '/sys/params/update';
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// API 函数
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/** 配置列表分页查询 */
|
||||||
|
export function getParamList(params: ParamQueryParams): Promise<ApiResult<PageData<SysParamsDTO>>> {
|
||||||
|
return get(PARAM_PAGE, params as Record<string, any>);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 新增配置 */
|
||||||
|
export function saveParam(params: ParamSaveParams): Promise<ApiResult<null>> {
|
||||||
|
return post(PARAM_SAVE, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 编辑配置 */
|
||||||
|
export function updateParam(params: ParamUpdateParams): Promise<ApiResult<null>> {
|
||||||
|
return post(PARAM_UPDATE, params);
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* 参数配置 - 表格列配置(视图层)
|
||||||
|
* 纯展示逻辑,不包含状态管理
|
||||||
|
*/
|
||||||
|
import type { SysParamsDTO } from './services';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 参数配置表格列定义
|
||||||
|
*
|
||||||
|
* 列:参数编码 / 参数值 / 备注说明
|
||||||
|
* 操作列在 index.tsx 中动态追加
|
||||||
|
*/
|
||||||
|
export function useParamsColumns() {
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: '参数编码',
|
||||||
|
dataIndex: 'paramCode',
|
||||||
|
key: 'paramCode',
|
||||||
|
width: 230,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '参数值',
|
||||||
|
dataIndex: 'paramValue',
|
||||||
|
key: 'paramValue',
|
||||||
|
width: 480,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '备注说明',
|
||||||
|
dataIndex: 'remark',
|
||||||
|
key: 'remark',
|
||||||
|
minWidth: 240,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return { columns };
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { computed, reactive, toRef, nextTick, type Ref } from 'vue';
|
||||||
|
import { useThrottleFn, useDebounce } from '@/hooks';
|
||||||
|
import { useRequest } from '@/hooks/useRequest';
|
||||||
|
import { useEffect } from '@/hooks/useEffect';
|
||||||
|
import { usePagination, syncPaginationTotal } from '@/hooks/usePagination';
|
||||||
|
import {
|
||||||
|
getParamList,
|
||||||
|
type SysParamsDTO,
|
||||||
|
type ParamQueryParams,
|
||||||
|
type PageData,
|
||||||
|
type ApiResult,
|
||||||
|
} from './services';
|
||||||
|
import { useParamsColumns } from './useParamsColumns';
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Model(业务逻辑 Hook)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 参数配置页数据模型
|
||||||
|
*/
|
||||||
|
export function useParamsModel() {
|
||||||
|
// ===== 筛选条件 =====
|
||||||
|
const filterForm = reactive({
|
||||||
|
paramCode: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 参数编码筛选防抖 300ms
|
||||||
|
const { debouncedValue: debouncedParamCode } = useDebounce(
|
||||||
|
toRef(filterForm, 'paramCode') as Ref<string>,
|
||||||
|
{ delay: 300 },
|
||||||
|
);
|
||||||
|
|
||||||
|
// ===== 分页 =====
|
||||||
|
const pagination = usePagination({ defaultCurrent: 1, defaultPageSize: 10 });
|
||||||
|
|
||||||
|
// ===== 构建 API 查询参数 =====
|
||||||
|
const buildQueryParams = (): ParamQueryParams => {
|
||||||
|
const params: ParamQueryParams = {
|
||||||
|
page: String((pagination as any).current.value),
|
||||||
|
limit: String((pagination as any).pageSize.value),
|
||||||
|
};
|
||||||
|
if (filterForm.paramCode.trim()) {
|
||||||
|
params.paramCode = filterForm.paramCode.trim();
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== 数据请求 =====
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
loading,
|
||||||
|
run: fetchList,
|
||||||
|
} = useRequest<ApiResult<PageData<SysParamsDTO>>>(() => getParamList(buildQueryParams()), {
|
||||||
|
refreshDeps: [],
|
||||||
|
formatResult: (res) => res,
|
||||||
|
});
|
||||||
|
|
||||||
|
const dataSource = computed(() => {
|
||||||
|
const res = data.value;
|
||||||
|
return res ? (res as any).data?.list || [] : [];
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
syncPaginationTotal(pagination, (data.value as any)?.data?.total);
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
// ===== 表格列配置 =====
|
||||||
|
const { columns } = useParamsColumns();
|
||||||
|
|
||||||
|
// ===== 计算属性 =====
|
||||||
|
const hasFilter = computed(() => {
|
||||||
|
return debouncedParamCode.value.trim() !== '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== 方法 =====
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
pagination.reset();
|
||||||
|
fetchList();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 重置(重置后自动查询) */
|
||||||
|
const handleReset = useThrottleFn(async () => {
|
||||||
|
filterForm.paramCode = '';
|
||||||
|
pagination.reset();
|
||||||
|
await fetchList();
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
/** 分页变更 */
|
||||||
|
const handlePageChange = (page: number, pageSize: number) => {
|
||||||
|
pagination.setCurrent(page);
|
||||||
|
if (pageSize !== (pagination as any).pageSize.value) {
|
||||||
|
pagination.setPageSize(pageSize);
|
||||||
|
}
|
||||||
|
nextTick().then(() => fetchList());
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
filterForm,
|
||||||
|
loading,
|
||||||
|
dataSource,
|
||||||
|
columns,
|
||||||
|
pagination,
|
||||||
|
hasFilter,
|
||||||
|
handleSearch,
|
||||||
|
handleReset,
|
||||||
|
handlePageChange,
|
||||||
|
fetchList,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
.bottom {
|
.bottom {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: flex-end;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
padding-top: 12px;
|
padding-top: 12px;
|
||||||
@@ -19,7 +19,8 @@
|
|||||||
|
|
||||||
.paginationRow {
|
.paginationRow {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,25 +152,22 @@ export default defineComponent({
|
|||||||
rowKey="phone"
|
rowKey="phone"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 底部 */}
|
{/* 分页 */}
|
||||||
<div class={styles.bottom}>
|
<div class={styles.paginationRow}>
|
||||||
<div class={styles.total}>共 {pagination.value.total} 人</div>
|
<div class={styles.total}>共 {pagination.value.total} 人</div>
|
||||||
<Button onClick={props.onClose}>关闭</Button>
|
<Pagination
|
||||||
|
current={pagination.value.current}
|
||||||
|
pageSize={pagination.value.pageSize}
|
||||||
|
total={pagination.value.total}
|
||||||
|
size="small"
|
||||||
|
onChange={handlePageChange}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 分页 */}
|
{/* 底部 */}
|
||||||
{pagination.value.total > pagination.value.pageSize && (
|
<div class={styles.bottom}>
|
||||||
<div class={styles.paginationRow}>
|
<Button onClick={props.onClose}>关闭</Button>
|
||||||
<Pagination
|
</div>
|
||||||
current={pagination.value.current}
|
|
||||||
pageSize={pagination.value.pageSize}
|
|
||||||
total={pagination.value.total}
|
|
||||||
size="small"
|
|
||||||
showTotal={(total: number) => `共 ${total} 条`}
|
|
||||||
onChange={handlePageChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -93,7 +93,10 @@ export function useRoleModel() {
|
|||||||
|
|
||||||
// ===== 方法 =====
|
// ===== 方法 =====
|
||||||
|
|
||||||
const handleSearch = () => fetchList();
|
const handleSearch = () => {
|
||||||
|
pagination.reset();
|
||||||
|
fetchList();
|
||||||
|
};
|
||||||
|
|
||||||
/** 重置(重置后自动查询) */
|
/** 重置(重置后自动查询) */
|
||||||
const handleReset = useThrottleFn(async () => {
|
const handleReset = useThrottleFn(async () => {
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ function renderBodyCell({
|
|||||||
)}
|
)}
|
||||||
{canToggleStatus && (
|
{canToggleStatus && (
|
||||||
<Button type="link" size="small" onClick={() => onToggleStatus(record)}>
|
<Button type="link" size="small" onClick={() => onToggleStatus(record)}>
|
||||||
{isActive ? '停用' : '启用'}
|
{isActive ? '禁用' : '启用'}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
@@ -92,17 +92,20 @@ export default defineComponent({
|
|||||||
const [editingRecord, setEditingRecord] = useState<any>(null);
|
const [editingRecord, setEditingRecord] = useState<any>(null);
|
||||||
const [formSubmitting, setFormSubmitting] = useState<boolean>(false);
|
const [formSubmitting, setFormSubmitting] = useState<boolean>(false);
|
||||||
|
|
||||||
// ===== 启用/禁用(使用真实 API) =====
|
// ===== 启用/禁用 =====
|
||||||
const handleToggleStatus = useThrottleFn(async (record: any) => {
|
const handleToggleStatus = useThrottleFn(async (record: any) => {
|
||||||
const isActive = record.status === '1';
|
const isActive = record.status === '1';
|
||||||
const actionText = isActive ? '停用' : '启用';
|
const actionText = isActive ? '禁用' : '启用';
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: `${actionText}确认`,
|
title: `确认${actionText}`,
|
||||||
content: (
|
content: (
|
||||||
<div class={pageStyles.confirmContent}>
|
<div class={pageStyles.confirmContent}>
|
||||||
<span>确定要{actionText}用户</span>
|
<span>确定要{actionText}用户</span>
|
||||||
<strong>{record.realName}</strong>
|
<strong>{record.realName}</strong>
|
||||||
<span>吗?</span>
|
<span>吗?</span>
|
||||||
|
<div class={pageStyles.confirmTip}>
|
||||||
|
{isActive ? '禁用后该用户将无法登录后台。' : '启用后该用户将恢复登录权限。'}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
okText: '确认',
|
okText: '确认',
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ const userUpdatePwd = '/admin/sys/user/updatepwd';
|
|||||||
/**
|
/**
|
||||||
* 用户列表分页
|
* 用户列表分页
|
||||||
* 接口: GET /admin/sys/user/page
|
* 接口: GET /admin/sys/user/page
|
||||||
* 参数: page, limit, text(姓名/手机号), roleId(角色ID), status(0=停用,1=正常)
|
* 参数: page, limit, text(姓名/手机号), roleId(角色ID), status(0=禁用,1=正常)
|
||||||
*/
|
*/
|
||||||
export function getUserList(
|
export function getUserList(
|
||||||
params: UserListQueryParams,
|
params: UserListQueryParams,
|
||||||
|
|||||||
@@ -21,14 +21,14 @@ import { getRoleList, type TournamentAdminRolePageVO } from '../../roles/model/s
|
|||||||
/** 状态选项(筛选) */
|
/** 状态选项(筛选) */
|
||||||
export const USER_STATUS_OPTIONS = [
|
export const USER_STATUS_OPTIONS = [
|
||||||
{ value: '', label: '全部' },
|
{ value: '', label: '全部' },
|
||||||
{ value: '0', label: '停用' },
|
{ value: '0', label: '禁用' },
|
||||||
{ value: '1', label: '正常' },
|
{ value: '1', label: '正常' },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
/** 状态文本/色调映射(表格渲染),对应 API status 字段:0=停用、1=正常 */
|
/** 状态文本/色调映射(表格渲染),对应 API status 字段:0=禁用、1=正常 */
|
||||||
const STATUS_MAP: Record<string, { label: string; tone: StatusTagTone }> = {
|
const STATUS_MAP: Record<string, { label: string; tone: StatusTagTone }> = {
|
||||||
'1': { label: '正常', tone: 'success' },
|
'1': { label: '正常', tone: 'success' },
|
||||||
'0': { label: '停用', tone: 'danger' },
|
'0': { label: '禁用', tone: 'danger' },
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -133,7 +133,10 @@ export function useUserModel() {
|
|||||||
|
|
||||||
// ===== 方法 =====
|
// ===== 方法 =====
|
||||||
|
|
||||||
const handleSearch = () => fetchList();
|
const handleSearch = () => {
|
||||||
|
pagination.reset();
|
||||||
|
fetchList();
|
||||||
|
};
|
||||||
|
|
||||||
/** 重置(节流 500ms,重置后自动查询) */
|
/** 重置(节流 500ms,重置后自动查询) */
|
||||||
const handleReset = useThrottleFn(async () => {
|
const handleReset = useThrottleFn(async () => {
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
declare module 'china-area-data' {
|
declare module 'china-area-data/data.json' {
|
||||||
const data: Record<string, Record<string, string>>;
|
const data: Record<string, Record<string, string>>;
|
||||||
export default data;
|
export default data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import areaData from 'china-area-data';
|
import areaData from 'china-area-data/data.json';
|
||||||
|
|
||||||
export interface CascadeOption {
|
export interface CascadeOption {
|
||||||
value: string;
|
value: string;
|
||||||
|
|||||||
@@ -33,6 +33,12 @@ export const REAL_NAME_REGEX = /^[\u4e00-\u9fa5A-Za-z0-9·]{1,8}$/;
|
|||||||
*/
|
*/
|
||||||
export const IMAGE_TYPE_REGEX = /^image\//;
|
export const IMAGE_TYPE_REGEX = /^image\//;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 纯数字(整数)
|
||||||
|
* - 用于过滤非数字字符
|
||||||
|
*/
|
||||||
|
export const INTEGER_ONLY_REGEX = /\D/g;
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 校验函数
|
// 校验函数
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -56,3 +62,8 @@ export function isValidRealName(value: unknown): boolean {
|
|||||||
export function isImageFile(file: { type: string }): boolean {
|
export function isImageFile(file: { type: string }): boolean {
|
||||||
return IMAGE_TYPE_REGEX.test(file.type);
|
return IMAGE_TYPE_REGEX.test(file.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 过滤非数字字符,只保留整数 */
|
||||||
|
export function toIntegerOnly(value: unknown): string {
|
||||||
|
return String(value ?? '').replace(INTEGER_ONLY_REGEX, '');
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user