36 Commits

Author SHA1 Message Date
chenzhen 64fdaba93d feat: 添加提现申请弹窗添加复制图片 2026-08-18 10:54:04 +08:00
chenzhen 50d5f54015 Merge pull request 'feat:amount optimization and change style optimization' (#14) from release/amountOptimization into master
Reviewed-on: #14
2026-08-10 21:23:14 +08:00
chenzhen a77592dec6 feat: change style optimization 2026-08-10 21:16:03 +08:00
chenzhen 00867ff960 feat: amount optimization 2026-08-10 20:42:51 +08:00
chenzhen 4c08bd1f8c Merge pull request '赛事运营平台 相关功能' (#13) from release/0.01 into master
Reviewed-on: #13
2026-08-10 16:02:03 +08:00
chenzhen ab6f5c787e Merge pull request 'Feature/0723/zr' (#12) from feature/0723/ZR into release/0.01
Reviewed-on: #12
2026-08-10 11:23:19 +08:00
ZhuRui 79a7ea8c56 fix: 文案修改 2026-08-10 09:44:45 +08:00
ZhuRui b5ba0ca3fb Merge remote-tracking branch 'origin/release/0.01' into feature/0723/ZR 2026-08-10 08:38:37 +08:00
ZhuRui 9b5e2807b5 fix: 处理部分前端问题 2026-08-08 18:40:52 +08:00
chenzhen 3a30e81183 Merge pull request 'fix: 声明改变' (#11) from feature/0723/ZR into release/0.01
Reviewed-on: #11
2026-08-08 15:08:52 +08:00
ZhuRui 28386f5429 fix: 声明改变 2026-08-08 15:04:26 +08:00
chenzhen b0b8689acb Merge pull request 'Feature/0723/zr' (#10) from feature/0723/ZR into release/0.01
Reviewed-on: #10
2026-08-08 14:51:53 +08:00
ZhuRui 8012cb82fc Merge remote-tracking branch 'origin/release/0.01' into feature/0723/ZR 2026-08-08 14:43:33 +08:00
ZhuRui 73b2da2d1e fix: 处理打包问题 2026-08-08 14:42:39 +08:00
chenzhen 6f8e5036f7 Merge pull request 'Feature/0723/zr' (#9) from feature/0723/ZR into release/0.01
Reviewed-on: #9
2026-08-08 14:23:41 +08:00
ZhuRui daab8ec41a fix: 更新 2026-08-08 14:22:32 +08:00
ZhuRui b513d701d8 fix: 更新 2026-08-08 14:22:17 +08:00
ZhuRui f8eaa554b3 fix: 合并更新 2026-08-08 14:11:16 +08:00
ZhuRui 9cee70bc62 fix: 处理部分优化问题 2026-08-08 10:38:38 +08:00
ZhuRui ffd917815d feat: 样式优化 2026-08-07 20:36:55 +08:00
ZhuRui 3841fcc7e5 fix: 字段补充 样式修改 2026-08-07 20:22:31 +08:00
ZhuRui ac7bfeacfe fix: 样式打包失效优化 2026-08-07 20:12:33 +08:00
ZhuRui d9a20c906b fix: 样式优化 2026-08-07 20:06:04 +08:00
ZhuRui 5300a1ec2e fix: 小改动 2026-08-07 19:43:40 +08:00
ZhuRui 68820dfcea fix: 样式修改 去除关闭所有标签页功能 2026-08-07 19:40:22 +08:00
ZhuRui d8ee7b3263 fix: 小改动 2026-08-07 18:42:06 +08:00
ZhuRui 3f47005f76 fix: 新增参数配置页面 处理其它问题 2026-08-07 18:19:14 +08:00
ZhuRui f0f0c88f18 fix: 小修改 2026-08-07 14:55:45 +08:00
ZhuRui 0f611ad926 fix: 修改权限提示 2026-08-07 14:48:25 +08:00
ZhuRui ba71b504e7 fix: 杂项修改 2026-08-07 14:30:54 +08:00
ZhuRui 117a7f408f fix: 对接登出 2026-08-07 11:26:49 +08:00
ZhuRui 05c3d518fa fix: 兼容后端 code 类型不统一问题 2026-08-07 11:20:30 +08:00
ZhuRui ea24625805 fix: 处理部分页面防抖滥用问题 2026-08-07 11:14:16 +08:00
ZhuRui 845829d088 fix: 优化部分样式与文案 2026-08-07 09:32:26 +08:00
ZhuRui 530de8b31c fix: api声明优化 接口请求错误兜底优化 2026-08-07 09:13:15 +08:00
ZhuRui 3cddae93aa fix: 优化赛事列表详情弹窗 2026-08-07 09:03:49 +08:00
57 changed files with 4017 additions and 216 deletions
+4 -1
View File
@@ -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
+1721
View File
File diff suppressed because it is too large Load Diff
+9
View File
@@ -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": {
+57
View File
@@ -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 # 参数配置-查看
+1045 -1
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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;
} }
+1 -1
View File
@@ -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;
/** 响应数据 */ /** 响应数据 */
+8 -1
View File
@@ -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;
}>;
};
+1 -1
View File
@@ -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
View File
@@ -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;
}); });
+1 -1
View File
@@ -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;
} }
+49
View File
@@ -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 });
}
+40
View File
@@ -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;
}
+1 -1
View File
@@ -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;
} }
+1 -1
View File
@@ -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;
} }
+1 -1
View File
@@ -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;
} }
+1 -1
View File
@@ -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;
} }
+3 -1
View File
@@ -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;
} }
/** 交易明细查询参数 */ /** 交易明细查询参数 */
+3 -1
View File
@@ -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;
} }
+4 -4
View File
@@ -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>
), ),
}} }}
+9
View File
@@ -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: '操作日志',
+9 -3
View File
@@ -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');
+4 -6
View File
@@ -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>
{/* 修改密码弹窗 */} {/* 修改密码弹窗 */}
@@ -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;
@@ -66,13 +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;
border-radius: 4px;
transition: all 0.2s ease;
&:hover {
color: #1677ff;
background: #e6f4ff;
}
} }
} }
@@ -1,6 +1,7 @@
import { defineComponent, computed, reactive, ref } from 'vue'; import { defineComponent, computed, reactive, ref } from 'vue';
import { Modal, Descriptions, Image, Table, Pagination, Spin, Space } 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,
@@ -43,6 +44,7 @@ const renderIdImages = (imgList: string[], singleImg: string) => {
width={40} width={40}
height={40} height={40}
style={{ objectFit: 'cover', borderRadius: '4px', cursor: 'pointer' }} style={{ objectFit: 'cover', borderRadius: '4px', cursor: 'pointer' }}
previewMask={() => <EyeOutlined />}
/> />
))} ))}
</Space> </Space>
@@ -79,6 +81,13 @@ const ID_CARD_IMG_COLUMN: any = {
record ? renderIdImages(record.idCardImgList || [], record.idCardImg || '') : <span>--</span>, 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({
name: 'EventDetailModal', name: 'EventDetailModal',
props: { props: {
@@ -92,12 +101,12 @@ export default defineComponent({
const [detailLoading, setDetailLoading] = useState(false); const [detailLoading, setDetailLoading] = useState(false);
/** 动态列:根据赛事是否要求证件号/证件图片决定显示 */ /** 动态列:根据赛事是否要求证件号/证件图片决定显示 */
const playerColumns = computed(() => { const playerColumns = computed(() => [
const cols = [...BASE_PLAYER_COLUMNS]; ...BASE_PLAYER_COLUMNS,
if (detail.value?.isRequireIdCardNo === '1') cols.push(ID_CARD_COLUMN); ...CONDITIONAL_PLAYER_COLUMNS.filter(({ key }) => detail.value?.[key] === '1').map(
if (detail.value?.isRequireIdCardImg === '1') cols.push(ID_CARD_IMG_COLUMN); ({ column }) => column,
return cols; ),
}); ]);
// ===== 每个组别的选手数据(key = categoryId ===== // ===== 每个组别的选手数据(key = categoryId =====
const categoryPlayers = ref<Record<string, TournamentAdminInfoSignupVO[]>>({}); const categoryPlayers = ref<Record<string, TournamentAdminInfoSignupVO[]>>({});
@@ -162,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) {
@@ -173,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
@@ -190,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}>
<div style={{ whiteSpace: 'pre-wrap', maxHeight: '280px', overflowY: 'auto' }}>
{detail.value.descr} {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="证件号">
@@ -219,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="最大人数"
@@ -233,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}>
@@ -245,12 +270,14 @@ export default defineComponent({
{/* ===== 封面 ===== */} {/* ===== 封面 ===== */}
<div class={styles.section}> <div class={styles.section}>
<div class={styles.sectionTitle}></div> <div class={styles.sectionTitle}></div>
<div class={styles.coverWrapper}>
<Image <Image
style={{ maxHeight: '380px' }} style={{ maxHeight: '380px' }}
src={detail.value.img || defaultCover} src={detail.value.img || defaultCover}
fallback={defaultCover} fallback={defaultCover}
/> />
</div> </div>
</div>
{/* ===== 赛事公告 ===== */} {/* ===== 赛事公告 ===== */}
{detail.value.notice && ( {detail.value.notice && (
@@ -276,7 +303,7 @@ export default defineComponent({
size="small" size="small"
pagination={false} pagination={false}
bordered bordered
scroll={{ x: 'max-content', y: 200 }} scroll={{ x: 'max-content' }}
/> />
{categoryPlayerTotal.value[cat.id] > 0 && ( {categoryPlayerTotal.value[cat.id] > 0 && (
<div class={styles.paginationRow}> <div class={styles.paginationRow}>
@@ -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 {
+1 -1
View File
@@ -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)}>
@@ -145,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}确认`,
@@ -157,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('');
} }
@@ -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 {
@@ -57,16 +58,19 @@ const PLAYER_COLUMNS = [
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>
); );
@@ -132,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;
}; };
@@ -106,7 +100,7 @@ export function usePaymentsModel() {
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 },
@@ -124,18 +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 = () => { const handleSearch = useThrottleFn(() => {
pagination.reset(); pagination.reset();
fetchList(); fetchList();
}; }, 300);
const handleReset = useThrottleFn(async () => { const handleReset = useThrottleFn(async () => {
filterForm.timeRange = null; filterForm.timeRange = null;
+6 -2
View File
@@ -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';
@@ -151,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',
+2 -1
View File
@@ -160,7 +160,8 @@ export default defineComponent({
placeholder="请输入" placeholder="请输入"
style={{ width: '140px' }} style={{ width: '140px' }}
value={filterForm.minBalance || undefined} value={filterForm.minBalance || undefined}
precision={0} precision={2}
step={0.01}
min={0} min={0}
controls={false} controls={false}
onUpdate:value={(val: any) => onUpdate:value={(val: any) =>
@@ -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,10 +166,10 @@ export function useWalletModel() {
// ===== 方法 ===== // ===== 方法 =====
const handleSearch = () => { const handleSearch = useThrottleFn(() => {
pagination.reset(); pagination.reset();
fetchList(); 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,6 +267,7 @@ export default defineComponent({
{auditForm.pass && <span class={styles.required}>*</span>} {auditForm.pass && <span class={styles.required}>*</span>}
</span> </span>
<div ref={uploadAreaRef} class={styles.uploadWrapper}>
<Upload <Upload
listType="picture-card" listType="picture-card"
fileList={imageList.value} fileList={imageList.value}
@@ -250,10 +279,15 @@ export default defineComponent({
{imageList.value.length >= MAX_IMAGE_COUNT ? null : ( {imageList.value.length >= MAX_IMAGE_COUNT ? null : (
<div class={styles.uploadTrigger}> <div class={styles.uploadTrigger}>
<PlusOutlined /> <PlusOutlined />
<div class={styles.uploadText}></div> <div class={styles.uploadText}></div>
</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;
@@ -169,9 +154,9 @@ export function useWithdrawModel() {
// ===== 计算属性 ===== // ===== 计算属性 =====
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,10 +164,10 @@ export function useWithdrawModel() {
// ===== 方法 ===== // ===== 方法 =====
const handleSearch = () => { const handleSearch = useThrottleFn(() => {
pagination.reset(); pagination.reset();
fetchList(); fetchList();
}; }, 300);
/** 重置(重置后自动查询) */ /** 重置(重置后自动查询) */
const handleReset = useThrottleFn(async () => { const handleReset = useThrottleFn(async () => {
+2 -2
View File
@@ -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>
+10 -14
View File
@@ -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,16 +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 = () => { const handleSearch = useThrottleFn(() => {
pagination.reset(); pagination.reset();
fetchList(); 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>
);
};
},
});
+33
View File
@@ -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;
}
}
+270
View File
@@ -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>
);
},
});
+29
View File
@@ -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条/页' },
];
+19
View File
@@ -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 个字' }];
+81
View File
@@ -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.total}> {pagination.value.total} </div>
<Button onClick={props.onClose}></Button>
</div>
{/* 分页 */} {/* 分页 */}
{pagination.value.total > pagination.value.pageSize && (
<div class={styles.paginationRow}> <div class={styles.paginationRow}>
<div class={styles.total}> {pagination.value.total} </div>
<Pagination <Pagination
current={pagination.value.current} current={pagination.value.current}
pageSize={pagination.value.pageSize} pageSize={pagination.value.pageSize}
total={pagination.value.total} total={pagination.value.total}
size="small" size="small"
showTotal={(total: number) => `${total}`}
onChange={handlePageChange} onChange={handlePageChange}
/> />
</div> </div>
)}
{/* 底部 */}
<div class={styles.bottom}>
<Button onClick={props.onClose}></Button>
</div>
</Modal> </Modal>
); );
}, },
+7 -4
View File
@@ -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: '确认',
+1 -1
View File
@@ -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,
+3 -3
View File
@@ -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' },
}; };
// ============================================================ // ============================================================
+1 -1
View File
@@ -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 -1
View File
@@ -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;