Refactor user search functionality in admin routes, enhance match challenge logic to prevent overlapping challenges, and update user-related data handling in the miniprogram. Improve UI consistency across various pages with design updates and optimize image upload responses to include full URLs.

This commit is contained in:
ethanfly 2026-01-26 05:12:26 +08:00
parent a3b4bd7cb0
commit 15413c85cc
43 changed files with 3747 additions and 74430 deletions

View File

@ -2,307 +2,370 @@
"hash": "0b0fcdca", "hash": "0b0fcdca",
"configHash": "0bd4dba1", "configHash": "0bd4dba1",
"lockfileHash": "45a4e0fd", "lockfileHash": "45a4e0fd",
"browserHash": "0f8eb059", "browserHash": "f9d297a9",
"optimized": { "optimized": {
"@element-plus/icons-vue": { "@element-plus/icons-vue": {
"src": "../../@element-plus/icons-vue/dist/index.js", "src": "../../@element-plus/icons-vue/dist/index.js",
"file": "@element-plus_icons-vue.js", "file": "@element-plus_icons-vue.js",
"fileHash": "63fef99e", "fileHash": "9f66cf3c",
"needsInterop": false "needsInterop": false
}, },
"axios": { "axios": {
"src": "../../axios/index.js", "src": "../../axios/index.js",
"file": "axios.js", "file": "axios.js",
"fileHash": "c4c91103", "fileHash": "d0e8827a",
"needsInterop": false "needsInterop": false
}, },
"dayjs": { "dayjs": {
"src": "../../dayjs/dayjs.min.js", "src": "../../dayjs/dayjs.min.js",
"file": "dayjs.js", "file": "dayjs.js",
"fileHash": "45ab92bc", "fileHash": "67e70480",
"needsInterop": true "needsInterop": true
}, },
"element-plus": { "element-plus": {
"src": "../../element-plus/es/index.mjs", "src": "../../element-plus/es/index.mjs",
"file": "element-plus.js", "file": "element-plus.js",
"fileHash": "a9eb7ac2", "fileHash": "6462e4a2",
"needsInterop": false "needsInterop": false
}, },
"element-plus/dist/locale/zh-cn.mjs": { "element-plus/dist/locale/zh-cn.mjs": {
"src": "../../element-plus/dist/locale/zh-cn.mjs", "src": "../../element-plus/dist/locale/zh-cn.mjs",
"file": "element-plus_dist_locale_zh-cn__mjs.js", "file": "element-plus_dist_locale_zh-cn__mjs.js",
"fileHash": "2e509bd7", "fileHash": "63d98427",
"needsInterop": false "needsInterop": false
}, },
"pinia": { "pinia": {
"src": "../../pinia/dist/pinia.mjs", "src": "../../pinia/dist/pinia.mjs",
"file": "pinia.js", "file": "pinia.js",
"fileHash": "5573d424", "fileHash": "1cc5fe52",
"needsInterop": false "needsInterop": false
}, },
"qrcode": { "qrcode": {
"src": "../../qrcode/lib/browser.js", "src": "../../qrcode/lib/browser.js",
"file": "qrcode.js", "file": "qrcode.js",
"fileHash": "2f402876", "fileHash": "8f92023e",
"needsInterop": true "needsInterop": true
}, },
"vue": { "vue": {
"src": "../../vue/dist/vue.runtime.esm-bundler.js", "src": "../../vue/dist/vue.runtime.esm-bundler.js",
"file": "vue.js", "file": "vue.js",
"fileHash": "0f609a55", "fileHash": "97abd90d",
"needsInterop": false "needsInterop": false
}, },
"vue-router": { "vue-router": {
"src": "../../vue-router/dist/vue-router.mjs", "src": "../../vue-router/dist/vue-router.mjs",
"file": "vue-router.js", "file": "vue-router.js",
"fileHash": "25c74ef8", "fileHash": "b189be4f",
"needsInterop": false "needsInterop": false
}, },
"element-plus/es": { "element-plus/es": {
"src": "../../element-plus/es/index.mjs", "src": "../../element-plus/es/index.mjs",
"file": "element-plus_es.js", "file": "element-plus_es.js",
"fileHash": "a13f022c", "fileHash": "4e5350bd",
"needsInterop": false "needsInterop": false
}, },
"element-plus/es/components/base/style/css": { "element-plus/es/components/base/style/css": {
"src": "../../element-plus/es/components/base/style/css.mjs", "src": "../../element-plus/es/components/base/style/css.mjs",
"file": "element-plus_es_components_base_style_css.js", "file": "element-plus_es_components_base_style_css.js",
"fileHash": "b4220b72", "fileHash": "2f24955e",
"needsInterop": false "needsInterop": false
}, },
"element-plus/es/components/loading/style/css": { "element-plus/es/components/loading/style/css": {
"src": "../../element-plus/es/components/loading/style/css.mjs", "src": "../../element-plus/es/components/loading/style/css.mjs",
"file": "element-plus_es_components_loading_style_css.js", "file": "element-plus_es_components_loading_style_css.js",
"fileHash": "0122de81", "fileHash": "5a7f8660",
"needsInterop": false "needsInterop": false
}, },
"element-plus/es/components/dialog/style/css": { "element-plus/es/components/dialog/style/css": {
"src": "../../element-plus/es/components/dialog/style/css.mjs", "src": "../../element-plus/es/components/dialog/style/css.mjs",
"file": "element-plus_es_components_dialog_style_css.js", "file": "element-plus_es_components_dialog_style_css.js",
"fileHash": "f74604cc", "fileHash": "27c0aa21",
"needsInterop": false "needsInterop": false
}, },
"element-plus/es/components/input/style/css": { "element-plus/es/components/input/style/css": {
"src": "../../element-plus/es/components/input/style/css.mjs", "src": "../../element-plus/es/components/input/style/css.mjs",
"file": "element-plus_es_components_input_style_css.js", "file": "element-plus_es_components_input_style_css.js",
"fileHash": "664f43b9", "fileHash": "1e44aa38",
"needsInterop": false "needsInterop": false
}, },
"element-plus/es/components/pagination/style/css": { "element-plus/es/components/pagination/style/css": {
"src": "../../element-plus/es/components/pagination/style/css.mjs", "src": "../../element-plus/es/components/pagination/style/css.mjs",
"file": "element-plus_es_components_pagination_style_css.js", "file": "element-plus_es_components_pagination_style_css.js",
"fileHash": "ff29c02c", "fileHash": "c46c3f28",
"needsInterop": false "needsInterop": false
}, },
"element-plus/es/components/table/style/css": { "element-plus/es/components/table/style/css": {
"src": "../../element-plus/es/components/table/style/css.mjs", "src": "../../element-plus/es/components/table/style/css.mjs",
"file": "element-plus_es_components_table_style_css.js", "file": "element-plus_es_components_table_style_css.js",
"fileHash": "9516c0f3", "fileHash": "f2cad5d9",
"needsInterop": false "needsInterop": false
}, },
"element-plus/es/components/tag/style/css": { "element-plus/es/components/tag/style/css": {
"src": "../../element-plus/es/components/tag/style/css.mjs", "src": "../../element-plus/es/components/tag/style/css.mjs",
"file": "element-plus_es_components_tag_style_css.js", "file": "element-plus_es_components_tag_style_css.js",
"fileHash": "acc11f71", "fileHash": "6026e653",
"needsInterop": false "needsInterop": false
}, },
"element-plus/es/components/table-column/style/css": { "element-plus/es/components/table-column/style/css": {
"src": "../../element-plus/es/components/table-column/style/css.mjs", "src": "../../element-plus/es/components/table-column/style/css.mjs",
"file": "element-plus_es_components_table-column_style_css.js", "file": "element-plus_es_components_table-column_style_css.js",
"fileHash": "63d6cae1", "fileHash": "20fe1fe1",
"needsInterop": false "needsInterop": false
}, },
"element-plus/es/components/form/style/css": { "element-plus/es/components/form/style/css": {
"src": "../../element-plus/es/components/form/style/css.mjs", "src": "../../element-plus/es/components/form/style/css.mjs",
"file": "element-plus_es_components_form_style_css.js", "file": "element-plus_es_components_form_style_css.js",
"fileHash": "8d500fe6", "fileHash": "acb1358d",
"needsInterop": false "needsInterop": false
}, },
"element-plus/es/components/form-item/style/css": { "element-plus/es/components/form-item/style/css": {
"src": "../../element-plus/es/components/form-item/style/css.mjs", "src": "../../element-plus/es/components/form-item/style/css.mjs",
"file": "element-plus_es_components_form-item_style_css.js", "file": "element-plus_es_components_form-item_style_css.js",
"fileHash": "dff655d7", "fileHash": "0d000125",
"needsInterop": false "needsInterop": false
}, },
"element-plus/es/components/select/style/css": { "element-plus/es/components/select/style/css": {
"src": "../../element-plus/es/components/select/style/css.mjs", "src": "../../element-plus/es/components/select/style/css.mjs",
"file": "element-plus_es_components_select_style_css.js", "file": "element-plus_es_components_select_style_css.js",
"fileHash": "18fa9e47", "fileHash": "f9c4f2b6",
"needsInterop": false "needsInterop": false
}, },
"element-plus/es/components/option/style/css": { "element-plus/es/components/option/style/css": {
"src": "../../element-plus/es/components/option/style/css.mjs", "src": "../../element-plus/es/components/option/style/css.mjs",
"file": "element-plus_es_components_option_style_css.js", "file": "element-plus_es_components_option_style_css.js",
"fileHash": "ec5bec4c", "fileHash": "5b17115f",
"needsInterop": false "needsInterop": false
}, },
"element-plus/es/components/button/style/css": { "element-plus/es/components/button/style/css": {
"src": "../../element-plus/es/components/button/style/css.mjs", "src": "../../element-plus/es/components/button/style/css.mjs",
"file": "element-plus_es_components_button_style_css.js", "file": "element-plus_es_components_button_style_css.js",
"fileHash": "4e0ee820", "fileHash": "072165a7",
"needsInterop": false "needsInterop": false
}, },
"element-plus/es/components/icon/style/css": { "element-plus/es/components/icon/style/css": {
"src": "../../element-plus/es/components/icon/style/css.mjs", "src": "../../element-plus/es/components/icon/style/css.mjs",
"file": "element-plus_es_components_icon_style_css.js", "file": "element-plus_es_components_icon_style_css.js",
"fileHash": "d81f84e6", "fileHash": "91400fe8",
"needsInterop": false "needsInterop": false
}, },
"element-plus/es/components/container/style/css": { "element-plus/es/components/container/style/css": {
"src": "../../element-plus/es/components/container/style/css.mjs", "src": "../../element-plus/es/components/container/style/css.mjs",
"file": "element-plus_es_components_container_style_css.js", "file": "element-plus_es_components_container_style_css.js",
"fileHash": "05c2c583", "fileHash": "42960329",
"needsInterop": false "needsInterop": false
}, },
"element-plus/es/components/main/style/css": { "element-plus/es/components/main/style/css": {
"src": "../../element-plus/es/components/main/style/css.mjs", "src": "../../element-plus/es/components/main/style/css.mjs",
"file": "element-plus_es_components_main_style_css.js", "file": "element-plus_es_components_main_style_css.js",
"fileHash": "a80d717e", "fileHash": "79d917a7",
"needsInterop": false "needsInterop": false
}, },
"element-plus/es/components/header/style/css": { "element-plus/es/components/header/style/css": {
"src": "../../element-plus/es/components/header/style/css.mjs", "src": "../../element-plus/es/components/header/style/css.mjs",
"file": "element-plus_es_components_header_style_css.js", "file": "element-plus_es_components_header_style_css.js",
"fileHash": "e687ec69", "fileHash": "784162ca",
"needsInterop": false "needsInterop": false
}, },
"element-plus/es/components/dropdown/style/css": { "element-plus/es/components/dropdown/style/css": {
"src": "../../element-plus/es/components/dropdown/style/css.mjs", "src": "../../element-plus/es/components/dropdown/style/css.mjs",
"file": "element-plus_es_components_dropdown_style_css.js", "file": "element-plus_es_components_dropdown_style_css.js",
"fileHash": "59835c76", "fileHash": "92871da6",
"needsInterop": false "needsInterop": false
}, },
"element-plus/es/components/dropdown-menu/style/css": { "element-plus/es/components/dropdown-menu/style/css": {
"src": "../../element-plus/es/components/dropdown-menu/style/css.mjs", "src": "../../element-plus/es/components/dropdown-menu/style/css.mjs",
"file": "element-plus_es_components_dropdown-menu_style_css.js", "file": "element-plus_es_components_dropdown-menu_style_css.js",
"fileHash": "23e3be80", "fileHash": "c0a9f980",
"needsInterop": false "needsInterop": false
}, },
"element-plus/es/components/dropdown-item/style/css": { "element-plus/es/components/dropdown-item/style/css": {
"src": "../../element-plus/es/components/dropdown-item/style/css.mjs", "src": "../../element-plus/es/components/dropdown-item/style/css.mjs",
"file": "element-plus_es_components_dropdown-item_style_css.js", "file": "element-plus_es_components_dropdown-item_style_css.js",
"fileHash": "85b49429", "fileHash": "846c18b1",
"needsInterop": false "needsInterop": false
}, },
"element-plus/es/components/avatar/style/css": { "element-plus/es/components/avatar/style/css": {
"src": "../../element-plus/es/components/avatar/style/css.mjs", "src": "../../element-plus/es/components/avatar/style/css.mjs",
"file": "element-plus_es_components_avatar_style_css.js", "file": "element-plus_es_components_avatar_style_css.js",
"fileHash": "936aa490", "fileHash": "094b2abf",
"needsInterop": false "needsInterop": false
}, },
"element-plus/es/components/breadcrumb/style/css": { "element-plus/es/components/breadcrumb/style/css": {
"src": "../../element-plus/es/components/breadcrumb/style/css.mjs", "src": "../../element-plus/es/components/breadcrumb/style/css.mjs",
"file": "element-plus_es_components_breadcrumb_style_css.js", "file": "element-plus_es_components_breadcrumb_style_css.js",
"fileHash": "875c584d", "fileHash": "6f37c7f8",
"needsInterop": false "needsInterop": false
}, },
"element-plus/es/components/breadcrumb-item/style/css": { "element-plus/es/components/breadcrumb-item/style/css": {
"src": "../../element-plus/es/components/breadcrumb-item/style/css.mjs", "src": "../../element-plus/es/components/breadcrumb-item/style/css.mjs",
"file": "element-plus_es_components_breadcrumb-item_style_css.js", "file": "element-plus_es_components_breadcrumb-item_style_css.js",
"fileHash": "b43c6781", "fileHash": "8aabd46b",
"needsInterop": false "needsInterop": false
}, },
"element-plus/es/components/aside/style/css": { "element-plus/es/components/aside/style/css": {
"src": "../../element-plus/es/components/aside/style/css.mjs", "src": "../../element-plus/es/components/aside/style/css.mjs",
"file": "element-plus_es_components_aside_style_css.js", "file": "element-plus_es_components_aside_style_css.js",
"fileHash": "e712c224", "fileHash": "3976d9c5",
"needsInterop": false "needsInterop": false
}, },
"element-plus/es/components/menu/style/css": { "element-plus/es/components/menu/style/css": {
"src": "../../element-plus/es/components/menu/style/css.mjs", "src": "../../element-plus/es/components/menu/style/css.mjs",
"file": "element-plus_es_components_menu_style_css.js", "file": "element-plus_es_components_menu_style_css.js",
"fileHash": "b95e1cd9", "fileHash": "2fd5552a",
"needsInterop": false "needsInterop": false
}, },
"element-plus/es/components/menu-item/style/css": { "element-plus/es/components/menu-item/style/css": {
"src": "../../element-plus/es/components/menu-item/style/css.mjs", "src": "../../element-plus/es/components/menu-item/style/css.mjs",
"file": "element-plus_es_components_menu-item_style_css.js", "file": "element-plus_es_components_menu-item_style_css.js",
"fileHash": "c8593318", "fileHash": "c1f41a14",
"needsInterop": false "needsInterop": false
}, },
"element-plus/es/components/input-number/style/css": { "element-plus/es/components/input-number/style/css": {
"src": "../../element-plus/es/components/input-number/style/css.mjs", "src": "../../element-plus/es/components/input-number/style/css.mjs",
"file": "element-plus_es_components_input-number_style_css.js", "file": "element-plus_es_components_input-number_style_css.js",
"fileHash": "67b6ba2c", "fileHash": "f2f57f0e",
"needsInterop": false "needsInterop": false
}, },
"element-plus/es/components/row/style/css": { "element-plus/es/components/row/style/css": {
"src": "../../element-plus/es/components/row/style/css.mjs", "src": "../../element-plus/es/components/row/style/css.mjs",
"file": "element-plus_es_components_row_style_css.js", "file": "element-plus_es_components_row_style_css.js",
"fileHash": "4e4350dd", "fileHash": "d116f174",
"needsInterop": false "needsInterop": false
}, },
"element-plus/es/components/col/style/css": { "element-plus/es/components/col/style/css": {
"src": "../../element-plus/es/components/col/style/css.mjs", "src": "../../element-plus/es/components/col/style/css.mjs",
"file": "element-plus_es_components_col_style_css.js", "file": "element-plus_es_components_col_style_css.js",
"fileHash": "43cd095d", "fileHash": "df159c48",
"needsInterop": false "needsInterop": false
}, },
"element-plus/es/components/radio-group/style/css": { "element-plus/es/components/radio-group/style/css": {
"src": "../../element-plus/es/components/radio-group/style/css.mjs", "src": "../../element-plus/es/components/radio-group/style/css.mjs",
"file": "element-plus_es_components_radio-group_style_css.js", "file": "element-plus_es_components_radio-group_style_css.js",
"fileHash": "86c77dd9", "fileHash": "a88d5658",
"needsInterop": false "needsInterop": false
}, },
"element-plus/es/components/radio/style/css": { "element-plus/es/components/radio/style/css": {
"src": "../../element-plus/es/components/radio/style/css.mjs", "src": "../../element-plus/es/components/radio/style/css.mjs",
"file": "element-plus_es_components_radio_style_css.js", "file": "element-plus_es_components_radio_style_css.js",
"fileHash": "e7a084fb", "fileHash": "a04c3aea",
"needsInterop": false
},
"element-plus/es/components/divider/style/css": {
"src": "../../element-plus/es/components/divider/style/css.mjs",
"file": "element-plus_es_components_divider_style_css.js",
"fileHash": "f4ed4f68",
"needsInterop": false
},
"element-plus/es/components/autocomplete/style/css": {
"src": "../../element-plus/es/components/autocomplete/style/css.mjs",
"file": "element-plus_es_components_autocomplete_style_css.js",
"fileHash": "44ebd843",
"needsInterop": false
},
"element-plus/es/components/text/style/css": {
"src": "../../element-plus/es/components/text/style/css.mjs",
"file": "element-plus_es_components_text_style_css.js",
"fileHash": "3ad4a3c9",
"needsInterop": false
},
"element-plus/es/components/switch/style/css": {
"src": "../../element-plus/es/components/switch/style/css.mjs",
"file": "element-plus_es_components_switch_style_css.js",
"fileHash": "2253b6b1",
"needsInterop": false
},
"element-plus/es/components/descriptions/style/css": {
"src": "../../element-plus/es/components/descriptions/style/css.mjs",
"file": "element-plus_es_components_descriptions_style_css.js",
"fileHash": "9c51f142",
"needsInterop": false
},
"element-plus/es/components/descriptions-item/style/css": {
"src": "../../element-plus/es/components/descriptions-item/style/css.mjs",
"file": "element-plus_es_components_descriptions-item_style_css.js",
"fileHash": "48d89583",
"needsInterop": false
},
"element-plus/es/components/upload/style/css": {
"src": "../../element-plus/es/components/upload/style/css.mjs",
"file": "element-plus_es_components_upload_style_css.js",
"fileHash": "bf70a099",
"needsInterop": false
},
"element-plus/es/components/image/style/css": {
"src": "../../element-plus/es/components/image/style/css.mjs",
"file": "element-plus_es_components_image_style_css.js",
"fileHash": "d3299a84",
"needsInterop": false
},
"element-plus/es/components/collapse/style/css": {
"src": "../../element-plus/es/components/collapse/style/css.mjs",
"file": "element-plus_es_components_collapse_style_css.js",
"fileHash": "b737d8d8",
"needsInterop": false
},
"element-plus/es/components/collapse-item/style/css": {
"src": "../../element-plus/es/components/collapse-item/style/css.mjs",
"file": "element-plus_es_components_collapse-item_style_css.js",
"fileHash": "ac7c5b8f",
"needsInterop": false "needsInterop": false
} }
}, },
"chunks": { "chunks": {
"chunk-4PW274X2": {
"file": "chunk-4PW274X2.js"
},
"chunk-JUCAMQ7P": {
"file": "chunk-JUCAMQ7P.js"
},
"chunk-SMFPDFTD": { "chunk-SMFPDFTD": {
"file": "chunk-SMFPDFTD.js" "file": "chunk-SMFPDFTD.js"
}, },
"chunk-75C4BP7B": {
"file": "chunk-75C4BP7B.js"
},
"chunk-5KK3TTMN": {
"file": "chunk-5KK3TTMN.js"
},
"chunk-UBLR4G7Q": {
"file": "chunk-UBLR4G7Q.js"
},
"chunk-NKQWFVTF": {
"file": "chunk-NKQWFVTF.js"
},
"chunk-R5DNQ3QC": { "chunk-R5DNQ3QC": {
"file": "chunk-R5DNQ3QC.js" "file": "chunk-R5DNQ3QC.js"
}, },
"chunk-B2YDYSZR": { "chunk-B2YDYSZR": {
"file": "chunk-B2YDYSZR.js" "file": "chunk-B2YDYSZR.js"
}, },
"chunk-75C4BP7B": {
"file": "chunk-75C4BP7B.js"
},
"chunk-5KK3TTMN": {
"file": "chunk-5KK3TTMN.js"
},
"chunk-REWOA3VH": { "chunk-REWOA3VH": {
"file": "chunk-REWOA3VH.js" "file": "chunk-REWOA3VH.js"
}, },
"chunk-TX5YLZ4O": { "chunk-TX5YLZ4O": {
"file": "chunk-TX5YLZ4O.js" "file": "chunk-TX5YLZ4O.js"
}, },
"chunk-4PW274X2": { "chunk-UBLR4G7Q": {
"file": "chunk-4PW274X2.js" "file": "chunk-UBLR4G7Q.js"
},
"chunk-IV6PSERC": {
"file": "chunk-IV6PSERC.js"
},
"chunk-W7GFOP2W": {
"file": "chunk-W7GFOP2W.js"
},
"chunk-OP4ZUAFM": {
"file": "chunk-OP4ZUAFM.js"
}, },
"chunk-YFT6OQ5R": { "chunk-YFT6OQ5R": {
"file": "chunk-YFT6OQ5R.js" "file": "chunk-YFT6OQ5R.js"
}, },
"chunk-NKQWFVTF": {
"file": "chunk-NKQWFVTF.js"
},
"chunk-IV6PSERC": {
"file": "chunk-IV6PSERC.js"
},
"chunk-STH2JMDO": {
"file": "chunk-STH2JMDO.js"
},
"chunk-HYZ2CRGS": { "chunk-HYZ2CRGS": {
"file": "chunk-HYZ2CRGS.js" "file": "chunk-HYZ2CRGS.js"
}, },
"chunk-H2732BJL": { "chunk-OP4ZUAFM": {
"file": "chunk-H2732BJL.js" "file": "chunk-OP4ZUAFM.js"
}, },
"chunk-QZC7O2C6": { "chunk-QZC7O2C6": {
"file": "chunk-QZC7O2C6.js" "file": "chunk-QZC7O2C6.js"
}, },
"chunk-H2732BJL": {
"file": "chunk-H2732BJL.js"
},
"chunk-G3PMV62Z": { "chunk-G3PMV62Z": {
"file": "chunk-G3PMV62Z.js" "file": "chunk-G3PMV62Z.js"
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -514,11 +514,11 @@ import {
virtualizedScrollbarProps, virtualizedScrollbarProps,
watermarkProps, watermarkProps,
zIndexContextKey zIndexContextKey
} from "./chunk-W7GFOP2W.js"; } from "./chunk-STH2JMDO.js";
import "./chunk-OP4ZUAFM.js";
import "./chunk-HYZ2CRGS.js"; import "./chunk-HYZ2CRGS.js";
import "./chunk-H2732BJL.js"; import "./chunk-OP4ZUAFM.js";
import "./chunk-QZC7O2C6.js"; import "./chunk-QZC7O2C6.js";
import "./chunk-H2732BJL.js";
import "./chunk-G3PMV62Z.js"; import "./chunk-G3PMV62Z.js";
var export_dayjs = import_dayjs.default; var export_dayjs = import_dayjs.default;
export { export {

View File

@ -514,11 +514,11 @@ import {
virtualizedScrollbarProps, virtualizedScrollbarProps,
watermarkProps, watermarkProps,
zIndexContextKey zIndexContextKey
} from "./chunk-W7GFOP2W.js"; } from "./chunk-STH2JMDO.js";
import "./chunk-OP4ZUAFM.js";
import "./chunk-HYZ2CRGS.js"; import "./chunk-HYZ2CRGS.js";
import "./chunk-H2732BJL.js"; import "./chunk-OP4ZUAFM.js";
import "./chunk-QZC7O2C6.js"; import "./chunk-QZC7O2C6.js";
import "./chunk-H2732BJL.js";
import "./chunk-G3PMV62Z.js"; import "./chunk-G3PMV62Z.js";
var export_dayjs = import_dayjs.default; var export_dayjs = import_dayjs.default;
export { export {

View File

@ -1,9 +1,9 @@
import "./chunk-75C4BP7B.js"; import "./chunk-75C4BP7B.js";
import "./chunk-5KK3TTMN.js"; import "./chunk-5KK3TTMN.js";
import "./chunk-UBLR4G7Q.js";
import "./chunk-NKQWFVTF.js";
import "./chunk-REWOA3VH.js"; import "./chunk-REWOA3VH.js";
import "./chunk-TX5YLZ4O.js"; import "./chunk-TX5YLZ4O.js";
import "./chunk-UBLR4G7Q.js";
import "./chunk-NKQWFVTF.js";
import "./chunk-IV6PSERC.js"; import "./chunk-IV6PSERC.js";
// node_modules/element-plus/es/components/pagination/style/css.mjs // node_modules/element-plus/es/components/pagination/style/css.mjs

View File

@ -1,7 +1,7 @@
import "./chunk-75C4BP7B.js"; import "./chunk-75C4BP7B.js";
import "./chunk-5KK3TTMN.js"; import "./chunk-5KK3TTMN.js";
import "./chunk-UBLR4G7Q.js";
import "./chunk-REWOA3VH.js"; import "./chunk-REWOA3VH.js";
import "./chunk-TX5YLZ4O.js"; import "./chunk-TX5YLZ4O.js";
import "./chunk-UBLR4G7Q.js";
import "./chunk-IV6PSERC.js"; import "./chunk-IV6PSERC.js";
//# sourceMappingURL=element-plus_es_components_select_style_css.js.map //# sourceMappingURL=element-plus_es_components_select_style_css.js.map

View File

@ -1,5 +1,5 @@
import "./chunk-5KK3TTMN.js";
import "./chunk-B2YDYSZR.js"; import "./chunk-B2YDYSZR.js";
import "./chunk-5KK3TTMN.js";
import "./chunk-IV6PSERC.js"; import "./chunk-IV6PSERC.js";
// node_modules/element-plus/es/components/table-column/style/css.mjs // node_modules/element-plus/es/components/table-column/style/css.mjs

View File

@ -17,6 +17,7 @@ export const getDashboard = () => request.get('/admin/dashboard')
// === 用户管理 === // === 用户管理 ===
export const getUsers = params => request.get('/admin/users', { params }) export const getUsers = params => request.get('/admin/users', { params })
export const searchUsers = params => request.get('/admin/users/search', { params }) // 搜索用户(用于积分操作)
export const getUserDetail = id => request.get(`/admin/users/${id}`) export const getUserDetail = id => request.get(`/admin/users/${id}`)
export const updateUserStatus = (id, data) => request.put(`/admin/users/${id}/status`, data) export const updateUserStatus = (id, data) => request.put(`/admin/users/${id}/status`, data)

File diff suppressed because it is too large Load Diff

View File

@ -89,6 +89,29 @@ App({
if (loginRes.data.code === 0) { if (loginRes.data.code === 0) {
this.globalData.token = loginRes.data.data.token; this.globalData.token = loginRes.data.data.token;
this.globalData.userInfo = loginRes.data.data.userInfo; this.globalData.userInfo = loginRes.data.data.userInfo;
// 处理天梯用户信息
if (loginRes.data.data.userInfo.ladderUsers && loginRes.data.data.userInfo.ladderUsers.length > 0) {
// 如果有当前门店,优先选择当前门店的天梯用户
if (this.globalData.currentStore?.storeId) {
const currentStoreLadderUser = loginRes.data.data.userInfo.ladderUsers.find(
lu => lu.storeId === this.globalData.currentStore.storeId
);
if (currentStoreLadderUser) {
this.globalData.ladderUser = currentStoreLadderUser;
} else {
// 当前门店没有天梯用户,取第一个
this.globalData.ladderUser = loginRes.data.data.userInfo.ladderUsers[0];
}
} else {
// 没有当前门店,取第一个天梯用户
this.globalData.ladderUser = loginRes.data.data.userInfo.ladderUsers[0];
}
} else {
// 没有天梯用户
this.globalData.ladderUser = null;
}
wx.setStorageSync("token", loginRes.data.data.token); wx.setStorageSync("token", loginRes.data.data.token);
this.connectWebSocket(); this.connectWebSocket();
resolve(loginRes.data.data); resolve(loginRes.data.data);
@ -112,6 +135,29 @@ App({
this.request("/api/user/info") this.request("/api/user/info")
.then((res) => { .then((res) => {
this.globalData.userInfo = res.data; this.globalData.userInfo = res.data;
// 处理天梯用户信息
if (res.data.ladderUsers && res.data.ladderUsers.length > 0) {
// 如果有当前门店,优先选择当前门店的天梯用户
if (this.globalData.currentStore?.storeId) {
const currentStoreLadderUser = res.data.ladderUsers.find(
lu => lu.storeId === this.globalData.currentStore.storeId
);
if (currentStoreLadderUser) {
this.globalData.ladderUser = currentStoreLadderUser;
} else {
// 当前门店没有天梯用户,取第一个
this.globalData.ladderUser = res.data.ladderUsers[0];
}
} else {
// 没有当前门店,取第一个天梯用户
this.globalData.ladderUser = res.data.ladderUsers[0];
}
} else {
// 没有天梯用户
this.globalData.ladderUser = null;
}
this.connectWebSocket(); this.connectWebSocket();
resolve(res.data); resolve(res.data);
}) })
@ -131,9 +177,25 @@ App({
}) })
.then((res) => { .then((res) => {
this.globalData.currentStore = res.data; this.globalData.currentStore = res.data;
// 如果当前门店有 ladderUserId获取该门店的天梯用户信息
if (res.data?.ladderUserId) { if (res.data?.ladderUserId) {
this.getLadderUser(res.data.storeId); this.getLadderUser(res.data.storeId);
} else if (res.data?.storeId) {
// 如果当前门店没有 ladderUserId但用户信息中有该门店的天梯用户使用它
if (this.globalData.userInfo?.ladderUsers) {
const currentStoreLadderUser = this.globalData.userInfo.ladderUsers.find(
lu => lu.storeId === res.data.storeId
);
if (currentStoreLadderUser) {
this.globalData.ladderUser = currentStoreLadderUser;
} else {
// 当前门店没有天梯用户,清空
this.globalData.ladderUser = null;
}
}
} }
resolve(res.data); resolve(res.data);
}) })
.catch(reject); .catch(reject);
@ -143,6 +205,25 @@ App({
this.request("/api/user/current-store") this.request("/api/user/current-store")
.then((res) => { .then((res) => {
this.globalData.currentStore = res.data; this.globalData.currentStore = res.data;
// 如果当前门店有 ladderUserId获取该门店的天梯用户信息
if (res.data?.ladderUserId) {
this.getLadderUser(res.data.storeId);
} else if (res.data?.storeId) {
// 如果当前门店没有 ladderUserId但用户信息中有该门店的天梯用户使用它
if (this.globalData.userInfo?.ladderUsers) {
const currentStoreLadderUser = this.globalData.userInfo.ladderUsers.find(
lu => lu.storeId === res.data.storeId
);
if (currentStoreLadderUser) {
this.globalData.ladderUser = currentStoreLadderUser;
} else {
// 当前门店没有天梯用户,清空
this.globalData.ladderUser = null;
}
}
}
resolve(res.data); resolve(res.data);
}) })
.catch(reject); .catch(reject);
@ -214,23 +295,64 @@ App({
handleWsMessage(data) { handleWsMessage(data) {
switch (data.type) { switch (data.type) {
case "challenge_request": case "challenge_request":
// 收到挑战请求 // 收到挑战请求 - 使用自定义弹框
wx.showModal({ const challengeData = data.data;
title: "收到挑战", const pages = getCurrentPages();
content: `${data.data.challenger.realName} 向你发起挑战`, const currentPage = pages[pages.length - 1];
confirmText: "接受",
cancelText: "拒绝", // 如果当前页面有处理挑战请求的方法,调用它
success: (res) => { if (currentPage && typeof currentPage.handleChallengeRequest === 'function') {
this.request( currentPage.handleChallengeRequest(challengeData);
"/api/match/challenge/respond", } else {
{ // 否则使用系统弹框
match_id: data.data.matchId, wx.showModal({
accept: res.confirm, title: "收到挑战",
}, content: `${challengeData.challenger.realName}(Lv${challengeData.challenger.level}, 战力${challengeData.challenger.powerScore}) 向你发起挑战`,
"POST" confirmText: "接受",
); cancelText: "拒绝",
}, success: (res) => {
}); this.request(
"/api/match/challenge/respond",
{
match_id: challengeData.matchId,
accept: res.confirm,
},
"POST"
).then(() => {
if (res.confirm) {
wx.showToast({ title: '已接受挑战', icon: 'success' });
// 跳转到挑战赛详情
setTimeout(() => {
wx.navigateTo({
url: `/pages/match/challenge-detail/index?id=${challengeData.matchId}`
});
}, 1500);
} else {
wx.showToast({ title: '已拒绝挑战', icon: 'success' });
}
}).catch(err => {
console.error('响应挑战失败:', err);
wx.showToast({ title: '操作失败', icon: 'none' });
});
},
});
}
break;
case "challenge_accepted":
// 挑战被接受
wx.showToast({ title: '对方已接受挑战', icon: 'success' });
// 如果当前在挑战赛详情页面,刷新数据
const pages2 = getCurrentPages();
const currentPage2 = pages2[pages2.length - 1];
if (currentPage2 && currentPage2.route === 'pages/match/challenge-detail/index') {
if (typeof currentPage2.loadMatchDetail === 'function') {
currentPage2.loadMatchDetail();
}
}
break;
case "challenge_rejected":
// 挑战被拒绝
wx.showToast({ title: '对方已拒绝挑战', icon: 'none' });
break; break;
case "score_confirm_request": case "score_confirm_request":
// 收到比分确认请求 // 收到比分确认请求

View File

@ -3,6 +3,7 @@
"pages/index/index", "pages/index/index",
"pages/user/index", "pages/user/index",
"pages/match/challenge/index", "pages/match/challenge/index",
"pages/match/challenge-detail/index",
"pages/match/ranking/index", "pages/match/ranking/index",
"pages/match/history/index", "pages/match/history/index",
"pages/points/mall/index", "pages/points/mall/index",

View File

@ -376,10 +376,6 @@ page {
color: var(--accent); color: var(--accent);
} }
.power-change.positive::before {
content: '+';
}
.power-change.negative { .power-change.negative {
color: #FF4D4F; color: #FF4D4F;
} }

View File

@ -6,17 +6,17 @@
// 开发环境配置 // 开发环境配置
const devConfig = { const devConfig = {
// API 基础地址(本地开发) // API 基础地址(本地开发)
baseUrl: "http://localhost:3000", baseUrl: "https://yingsa-server.ethan.team",
// WebSocket 地址(本地开发) // WebSocket 地址(本地开发)
wsUrl: "ws://localhost:3000/ws", wsUrl: "wss://yingsa-server.ethan.team/ws",
}; };
// 生产环境配置 // 生产环境配置
const prodConfig = { const prodConfig = {
// API 基础地址(生产环境,请替换为实际域名) // API 基础地址(生产环境,请替换为实际域名)
baseUrl: "https://your-domain.com", baseUrl: "https://yingsa-server.ethan.team",
// WebSocket 地址(生产环境,请替换为实际域名) // WebSocket 地址(生产环境,请替换为实际域名)
wsUrl: "wss://your-domain.com/ws", wsUrl: "wss://yingsa-server.ethan.team/ws",
}; };
// 根据环境变量选择配置 // 根据环境变量选择配置

View File

@ -26,94 +26,85 @@
<!-- 主要内容区域 --> <!-- 主要内容区域 -->
<view class="main-content"> <view class="main-content">
<!-- 性别筛选标签 --> <!-- 性别筛选标签 - 吸附在顶部 -->
<view class="filter-bar animate-fadeInUp" style="animation-delay: 0.1s"> <view class="filter-bar-wrapper">
<view <view class="filter-bar animate-fadeInUp" style="animation-delay: 0.1s">
class="filter-item {{gender === '' ? 'active' : ''}}" <view
bindtap="setGender" class="filter-item {{gender === '' ? 'active' : ''}}"
data-gender="" bindtap="setGender"
> data-gender=""
全部 >
</view> 全部
<view </view>
class="filter-item {{gender === '1' ? 'active' : ''}}" <view
bindtap="setGender" class="filter-item {{gender === '1' ? 'active' : ''}}"
data-gender="1" bindtap="setGender"
> data-gender="1"
♂ 男子 >
</view> ♂ 男子
<view </view>
class="filter-item {{gender === '2' ? 'active' : ''}}" <view
bindtap="setGender" class="filter-item {{gender === '2' ? 'active' : ''}}"
data-gender="2" bindtap="setGender"
> data-gender="2"
♀ 女子 >
♀ 女子
</view>
</view> </view>
</view> </view>
<!-- 排名列表 --> <!-- 排名列表 -->
<view class="ranking-list"> <view class="ranking-list">
<scroll-view <block wx:if="{{list.length > 0}}">
scroll-y="true" <view
class="ranking-scroll" class="ranking-item stagger-item {{index < 3 ? 'top-rank' : ''}} animate-fadeInUp"
bindscrolltolower="loadMore" wx:for="{{list}}"
lower-threshold="100" wx:key="id"
style="height: calc(100vh - 480rpx);" bindtap="viewPlayer"
> data-id="{{item.id}}"
<block wx:if="{{list.length > 0}}"> >
<view <!-- 排名徽章 -->
class="ranking-item stagger-item {{index < 3 ? 'top-rank' : ''}} animate-fadeInUp" <view class="rank-badge {{item.rank === 1 ? 'top1' : item.rank === 2 ? 'top2' : item.rank === 3 ? 'top3' : 'normal'}}">
wx:for="{{list}}" <text wx:if="{{item.rank <= 3}}">{{item.rank === 1 ? '👑' : item.rank === 2 ? '🥈' : '🥉'}}</text>
wx:key="id" <text wx:else>{{item.rank}}</text>
bindtap="viewPlayer" </view>
data-id="{{item.id}}"
> <!-- 选手头像 -->
<!-- 排名徽章 --> <image class="player-avatar" src="{{item.avatar || '/images/avatar-default.svg'}}" mode="aspectFill"></image>
<view class="rank-badge {{item.rank === 1 ? 'top1' : item.rank === 2 ? 'top2' : item.rank === 3 ? 'top3' : 'normal'}}">
<text wx:if="{{item.rank <= 3}}">{{item.rank === 1 ? '👑' : item.rank === 2 ? '🥈' : '🥉'}}</text> <!-- 选手信息 -->
<text wx:else>{{item.rank}}</text> <view class="player-info">
</view> <text class="player-name">{{item.realName}}</text>
<view class="player-meta">
<!-- 选手头像 --> <text class="player-level lv{{item.level}}">Lv{{item.level}}</text>
<image class="player-avatar" src="{{item.avatar || '/images/avatar-default.svg'}}" mode="aspectFill"></image> <text class="player-stats">胜率 {{item.winRate}}%</text>
<!-- 选手信息 -->
<view class="player-info">
<text class="player-name">{{item.realName}}</text>
<view class="player-meta">
<text class="player-level lv{{item.level}}">Lv{{item.level}}</text>
<text class="player-stats">胜率 {{item.winRate}}%</text>
</view>
</view>
<!-- 战力值 -->
<view class="player-power">
<text class="power-value">{{item.powerScore}}</text>
<text class="power-label">战力</text>
</view> </view>
</view> </view>
</block>
<!-- 战力值 -->
<!-- 空状态 --> <view class="player-power">
<view wx:elif="{{!loading}}" class="empty-state"> <text class="power-value">{{item.powerScore}}</text>
<image class="empty-icon" src="/images/empty-ranking.svg" mode="aspectFit"></image> <text class="power-label">战力</text>
<text class="empty-title">暂无排名数据</text> </view>
<text class="empty-desc">每月完成3场比赛即可上榜</text>
</view> </view>
</block>
<!-- 加载更多 --> <!-- 空状态 -->
<view wx:if="{{loading}}" class="loading-state"> <view wx:elif="{{!loading}}" class="empty-state">
<text>加载中...</text> <image class="empty-icon" src="/images/empty-ranking.svg" mode="aspectFit"></image>
</view> <text class="empty-title">暂无排名数据</text>
<text class="empty-desc">每月完成3场比赛即可上榜</text>
</view>
<!-- 到底提示 --> <!-- 加载更多 -->
<view wx:if="{{list.length > 0 && !hasMore && !loading}}" class="load-more"> <view wx:if="{{loading}}" class="loading-state">
<text>— 已显示全部选手 —</text> <text>加载中...</text>
</view> </view>
<!-- 底部安全区域 --> <!-- 到底提示 -->
<view class="safe-bottom"></view> <view wx:if="{{list.length > 0 && !hasMore && !loading}}" class="load-more">
</scroll-view> <text>— 已显示全部选手 —</text>
</view>
</view> </view>
</view> </view>
</view> </view>

View File

@ -6,35 +6,22 @@
min-height: 100vh; min-height: 100vh;
background: var(--bg-page); background: var(--bg-page);
position: relative; position: relative;
overflow: hidden;
} }
/* 顶部装饰背景 */ /* 顶部装饰背景 */
.hero-section { .hero-section {
position: relative; position: relative;
padding: 32rpx 24rpx 24rpx; padding: 48rpx 24rpx 32rpx;
background: linear-gradient(180deg, #FFF5F0 0%, var(--bg-page) 100%); background: transparent;
border-bottom: 1rpx solid rgba(0, 0, 0, 0.04);
} }
.hero-pattern { .hero-pattern {
position: absolute; display: none;
top: -80rpx;
right: -60rpx;
width: 300rpx;
height: 300rpx;
background: radial-gradient(circle, rgba(255, 107, 53, 0.1) 0%, transparent 70%);
border-radius: 50%;
animation: pulse 4s ease-in-out infinite;
} }
.hero-pattern-2 { .hero-pattern-2 {
position: absolute; display: none;
top: 120rpx;
left: -80rpx;
width: 200rpx;
height: 200rpx;
background: radial-gradient(circle, rgba(0, 201, 167, 0.06) 0%, transparent 70%);
border-radius: 50%;
} }
/* 门店信息头部 */ /* 门店信息头部 */
@ -44,7 +31,11 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
margin-bottom: 20rpx; margin-bottom: 32rpx;
padding: 16rpx 20rpx;
background: var(--bg-white);
border-radius: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
animation: fadeInUp 0.5s cubic-bezier(0.4, 0, 0.2, 1); animation: fadeInUp 0.5s cubic-bezier(0.4, 0, 0.2, 1);
} }
@ -58,24 +49,25 @@
width: 12rpx; width: 12rpx;
height: 12rpx; height: 12rpx;
border-radius: 50%; border-radius: 50%;
background: var(--accent); background: #ff6b35;
box-shadow: 0 0 8rpx rgba(255, 107, 53, 0.4);
animation: pulse 2s ease-in-out infinite; animation: pulse 2s ease-in-out infinite;
} }
.store-name { .store-name {
font-size: 28rpx; font-size: 30rpx;
font-weight: 600; font-weight: 600;
color: var(--text-primary); color: #333;
letter-spacing: 0.5rpx;
} }
.change-store-btn { .change-store-btn {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6rpx; gap: 6rpx;
padding: 12rpx 20rpx; padding: 10rpx 18rpx;
background: var(--bg-white); background: var(--bg-soft);
border-radius: var(--radius-full); border-radius: 20rpx;
box-shadow: var(--shadow-sm);
transition: all 0.3s ease; transition: all 0.3s ease;
} }
@ -85,12 +77,14 @@
.change-store-text { .change-store-text {
font-size: 24rpx; font-size: 24rpx;
color: var(--text-secondary); color: #666;
font-weight: 500;
} }
.change-store-arrow { .change-store-arrow {
font-size: 20rpx; font-size: 22rpx;
color: var(--text-muted); color: #999;
font-weight: 300;
} }
/* 页面标题 */ /* 页面标题 */
@ -98,34 +92,48 @@
position: relative; position: relative;
z-index: 1; z-index: 1;
text-align: center; text-align: center;
margin-bottom: 8rpx; margin-bottom: 0;
padding: 24rpx 0;
} }
.page-title { .page-title {
display: block; display: block;
font-size: 44rpx; font-size: 52rpx;
font-weight: 700; font-weight: 700;
color: var(--text-primary); color: #1a1a1a;
margin-bottom: 8rpx; margin-bottom: 8rpx;
letter-spacing: 2rpx; letter-spacing: 1rpx;
} }
.page-subtitle { .page-subtitle {
display: block; display: block;
font-size: 26rpx; font-size: 26rpx;
color: var(--text-muted); color: #999;
font-weight: 400;
letter-spacing: 0.5rpx;
} }
/* 主要内容区域 */ /* 主要内容区域 */
.main-content { .main-content {
padding: 0 24rpx; position: relative;
z-index: 1;
}
/* 筛选标签栏容器 - 用于吸附效果 */
.filter-bar-wrapper {
position: sticky;
top: 0;
z-index: 100;
background: var(--bg-page);
padding: 24rpx 24rpx;
/* margin-bottom: 24rpx; */
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
} }
/* 筛选标签栏 */ /* 筛选标签栏 */
.filter-bar { .filter-bar {
display: flex; display: flex;
gap: 16rpx; gap: 16rpx;
margin-bottom: 24rpx;
} }
.filter-item { .filter-item {
@ -141,9 +149,10 @@
} }
.filter-item.active { .filter-item.active {
background: var(--primary-gradient); background: linear-gradient(135deg, #ff6b35 0%, #ff8c42 100%);
color: var(--text-white); color: #fff;
box-shadow: var(--shadow-primary); box-shadow: 0 4rpx 16rpx rgba(255, 107, 53, 0.3);
font-weight: 600;
} }
.filter-item:active { .filter-item:active {
@ -152,82 +161,93 @@
/* 排名列表 */ /* 排名列表 */
.ranking-list { .ranking-list {
padding: 0 24rpx 40rpx;
}
.ranking-scroll {
} }
.ranking-item { .ranking-item {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 20rpx 24rpx; padding: 24rpx;
background: var(--bg-white); background: #fff;
border-radius: var(--radius-lg); border-radius: 20rpx;
margin-bottom: 12rpx; margin-bottom: 16rpx;
box-shadow: var(--shadow-sm); box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border: 1rpx solid #f0f0f0;
}
.ranking-item:last-child {
margin-bottom: 0;
} }
.ranking-item:active { .ranking-item:active {
transform: scale(0.98); transform: scale(0.98);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.12);
} }
.ranking-item.top-rank { .ranking-item.top-rank {
background: linear-gradient(135deg, #FFFBF8, var(--bg-white)); background: linear-gradient(135deg, #fff5f0 0%, #fff 100%);
border: 1rpx solid rgba(255, 107, 53, 0.1); border: 2rpx solid rgba(255, 107, 53, 0.2);
box-shadow: 0 6rpx 20rpx rgba(255, 107, 53, 0.15);
} }
/* 排名徽章 */ /* 排名徽章 */
.rank-badge { .rank-badge {
width: 52rpx; width: 56rpx;
height: 52rpx; height: 56rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border-radius: 50%; border-radius: 50%;
font-size: 26rpx; font-size: 26rpx;
font-weight: 700; font-weight: 700;
margin-right: 16rpx; margin-right: 20rpx;
flex-shrink: 0; flex-shrink: 0;
} }
.rank-badge.top1 { .rank-badge.top1 {
background: linear-gradient(135deg, #FFE082 0%, #FFD700 100%); background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
color: #8B4513; color: #8B4513;
box-shadow: 0 4rpx 16rpx rgba(255, 215, 0, 0.4); box-shadow: 0 6rpx 20rpx rgba(255, 215, 0, 0.5);
font-size: 28rpx; font-size: 32rpx;
width: 64rpx;
height: 64rpx;
} }
.rank-badge.top2 { .rank-badge.top2 {
background: linear-gradient(135deg, #F5F5F5 0%, #C0C0C0 100%); background: linear-gradient(135deg, #E8E8E8 0%, #C0C0C0 100%);
color: #4A4A4A; color: #4A4A4A;
box-shadow: 0 4rpx 12rpx rgba(192, 192, 192, 0.4); box-shadow: 0 4rpx 16rpx rgba(192, 192, 192, 0.5);
font-size: 28rpx; font-size: 32rpx;
width: 64rpx;
height: 64rpx;
} }
.rank-badge.top3 { .rank-badge.top3 {
background: linear-gradient(135deg, #DEB887 0%, #CD853F 100%); background: linear-gradient(135deg, #CD853F 0%, #B8860B 100%);
color: #5C4033; color: #fff;
box-shadow: 0 4rpx 12rpx rgba(205, 133, 63, 0.4); box-shadow: 0 4rpx 16rpx rgba(205, 133, 63, 0.5);
font-size: 28rpx; font-size: 32rpx;
width: 64rpx;
height: 64rpx;
} }
.rank-badge.normal { .rank-badge.normal {
background: var(--bg-soft); background: linear-gradient(135deg, #f5f5f5 0%, #e8e8e8 100%);
color: var(--text-muted); color: #666;
font-weight: 600;
} }
/* 选手头像 */ /* 选手头像 */
.player-avatar { .player-avatar {
width: 72rpx; width: 80rpx;
height: 72rpx; height: 80rpx;
border-radius: 50%; border-radius: 50%;
margin-right: 16rpx; margin-right: 20rpx;
border: 2rpx solid var(--bg-white); border: 3rpx solid #fff;
box-shadow: var(--shadow-sm); box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
flex-shrink: 0; flex-shrink: 0;
background: #f5f5f5;
} }
/* 选手信息 */ /* 选手信息 */
@ -238,10 +258,10 @@
.player-name { .player-name {
display: block; display: block;
font-size: 28rpx; font-size: 30rpx;
font-weight: 600; font-weight: 600;
color: var(--text-primary); color: #333;
margin-bottom: 6rpx; margin-bottom: 8rpx;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
@ -250,15 +270,18 @@
.player-meta { .player-meta {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12rpx; gap: 16rpx;
flex-wrap: wrap;
} }
.player-level { .player-level {
display: inline-flex; display: inline-flex;
padding: 4rpx 12rpx; align-items: center;
border-radius: var(--radius-full); padding: 4rpx 14rpx;
font-size: 20rpx; border-radius: 20rpx;
font-size: 22rpx;
font-weight: 600; font-weight: 600;
line-height: 1.2;
} }
.player-level.lv1 { background: #E8F5E9; color: #2E7D32; } .player-level.lv1 { background: #E8F5E9; color: #2E7D32; }
@ -268,8 +291,9 @@
.player-level.lv5 { background: #F3E5F5; color: #7B1FA2; } .player-level.lv5 { background: #F3E5F5; color: #7B1FA2; }
.player-stats { .player-stats {
font-size: 22rpx; font-size: 24rpx;
color: var(--text-muted); color: #666;
font-weight: 500;
} }
/* 战力值 */ /* 战力值 */
@ -280,14 +304,17 @@
.power-value { .power-value {
display: block; display: block;
font-size: 32rpx; font-size: 36rpx;
font-weight: 700; font-weight: 700;
color: var(--primary); color: #ff6b35;
line-height: 1.2;
margin-bottom: 4rpx;
} }
.power-label { .power-label {
font-size: 22rpx; font-size: 22rpx;
color: var(--text-muted); color: #999;
font-weight: 500;
} }
/* 空状态 */ /* 空状态 */
@ -329,12 +356,8 @@
/* 加载更多 */ /* 加载更多 */
.load-more { .load-more {
text-align: center; text-align: center;
padding: 32rpx; padding: 16rpx 32rpx 8rpx;
color: var(--text-muted); color: var(--text-muted);
font-size: 26rpx; font-size: 26rpx;
} }
/* 底部安全区域 */
.safe-bottom {
height: 80rpx;
}

View File

@ -0,0 +1,588 @@
const app = getApp()
Page({
data: {
matchId: null,
matchInfo: null,
myRole: null, // 'challenger' | 'defender' | null
canAccept: false,
canReject: false,
canSubmitScore: false,
canConfirmScore: false,
showScoreModal: false,
myScore: '',
opponentScore: '',
loading: false
},
onLoad(options) {
if (options.id) {
this.setData({ matchId: options.id })
this.loadMatchDetail()
}
},
onShow() {
// 每次显示页面时刷新数据
if (this.data.matchId) {
this.loadMatchDetail()
}
},
// 加载比赛详情
async loadMatchDetail() {
this.setData({ loading: true })
try {
const res = await app.request(`/api/match/${this.data.matchId}`)
console.log('API完整响应:', JSON.stringify(res, null, 2))
// app.request 返回的是 { code: 0, message, data }
// 所以 res.data 才是真正的数据
const matchInfo = res.data
console.log('比赛详情数据:', matchInfo)
console.log('数据字段:', Object.keys(matchInfo || {}))
if (!matchInfo) {
console.error('比赛详情数据为空')
wx.showToast({ title: '数据格式错误', icon: 'none' })
this.setData({ loading: false })
return
}
// 检查关键字段
console.log('关键字段检查:', {
hasMyRole: 'myRole' in matchInfo,
myRole: matchInfo.myRole,
myRoleType: typeof matchInfo.myRole,
hasCanAccept: 'canAccept' in matchInfo,
canAccept: matchInfo.canAccept,
canAcceptType: typeof matchInfo.canAccept,
hasCanReject: 'canReject' in matchInfo,
canReject: matchInfo.canReject,
canRejectType: typeof matchInfo.canReject,
status: matchInfo.status,
statusType: typeof matchInfo.status
})
// 确保布尔值正确设置
let canAccept = Boolean(matchInfo.canAccept)
let canReject = Boolean(matchInfo.canReject)
let canSubmitScore = Boolean(matchInfo.canSubmitScore)
let canConfirmScore = Boolean(matchInfo.canConfirmScore)
let myRole = matchInfo.myRole || null
// 临时方案:如果 myRole 为 null尝试通过其他方式判断角色
// 适用于待接受状态status=0和进行中状态status=1
if (!myRole && (matchInfo.status === 0 || matchInfo.status === 1)) {
const currentUser = app.globalData.userInfo
console.log('尝试临时方案识别角色:', {
status: matchInfo.status,
currentUser: currentUser ? { id: currentUser.id, phone: currentUser.phone } : null,
defender: matchInfo.defender ? { userId: matchInfo.defender.userId, phone: matchInfo.defender.phone } : null,
challenger: matchInfo.challenger ? { userId: matchInfo.challenger.userId, phone: matchInfo.challenger.phone } : null
})
if (currentUser) {
// 尝试通过 user_id 判断
if (matchInfo.defender && matchInfo.defender.userId && matchInfo.defender.userId == currentUser.id) {
myRole = 'defender'
console.log('临时方案通过defender.userId识别为被挑战者', {
defenderUserId: matchInfo.defender.userId,
currentUserId: currentUser.id
})
}
// 尝试通过手机号判断
else if (matchInfo.defender && currentUser.phone && matchInfo.defender.phone && matchInfo.defender.phone === currentUser.phone) {
myRole = 'defender'
console.log('临时方案:通过手机号识别为被挑战者', {
defenderPhone: matchInfo.defender.phone,
currentUserPhone: currentUser.phone
})
}
// 如果都不匹配,检查是否是挑战者
else if (matchInfo.challenger && matchInfo.challenger.userId && matchInfo.challenger.userId == currentUser.id) {
myRole = 'challenger'
console.log('临时方案识别为挑战者通过userId')
}
// 通过手机号识别挑战者
else if (matchInfo.challenger && currentUser.phone && matchInfo.challenger.phone && matchInfo.challenger.phone === currentUser.phone) {
myRole = 'challenger'
console.log('临时方案:识别为挑战者(通过手机号)')
}
}
}
// 如果状态是待接受且角色是被挑战者,强制设置权限(即使后端没有返回)
if (matchInfo.status === 0 && myRole === 'defender') {
canAccept = true
canReject = true
console.log('强制设置接受/拒绝权限(状态=0角色=defender')
}
// 最后的备用方案:如果状态是待接受,且当前用户不是挑战者,则可能是被挑战者
// 这种情况下,显示按钮让用户尝试接受/拒绝(如果用户不是被挑战者,后端会拒绝)
if (matchInfo.status === 0 && !myRole && !canAccept && !canReject) {
// 检查当前用户是否是挑战者
const currentUser = app.globalData.userInfo
const isChallenger = currentUser && matchInfo.challenger &&
(matchInfo.challenger.userId == currentUser.id ||
(currentUser.phone && matchInfo.challenger.phone === currentUser.phone))
// 如果不是挑战者,可能是被挑战者,显示按钮
if (!isChallenger) {
myRole = 'defender'
canAccept = true
canReject = true
console.log('备用方案:状态为待接受且不是挑战者,假设是被挑战者,显示按钮')
}
}
// 处理"进行中"状态status=1的操作权限
if (matchInfo.status === 1) {
const game = matchInfo.games && matchInfo.games[0]
if (game) {
console.log('处理进行中状态的操作权限:', {
gameStatus: game.status,
submitBy: game.submitBy,
confirmStatus: game.confirmStatus,
myRole,
canSubmitScore,
canConfirmScore
})
// 如果游戏状态为1进行中且未提交比分双方都可以填写比分
if (game.status === 1 && !game.submitBy) {
// 如果后端没有返回 canSubmitScore但状态允许则设置权限
// 即使 myRole 为 null也允许填写后续会通过游戏信息识别角色
if (!canSubmitScore) {
canSubmitScore = true
console.log('进行中状态:设置填写比分权限(游戏状态=1未提交')
}
}
// 如果游戏状态为2已提交且对方已提交等待我确认
else if (game.status === 2 && game.submitBy) {
// 判断当前用户是否是提交者submitBy 是 ladder_user_id
let isSubmitter = false
if (myRole) {
// 如果已识别角色,通过比较 submitBy 和 challenger/defender 的 idladder_user_id来判断
if (myRole === 'challenger' && matchInfo.challenger && game.submitBy == matchInfo.challenger.id) {
isSubmitter = true
console.log('当前用户是提交者(挑战者)')
} else if (myRole === 'defender' && matchInfo.defender && game.submitBy == matchInfo.defender.id) {
isSubmitter = true
console.log('当前用户是提交者(被挑战者)')
}
} else {
// 如果 myRole 为 null通过比较当前用户的 user_id 和 challenger/defender 的 user_id 来判断
const currentUser = app.globalData.userInfo
if (currentUser) {
// 检查提交者是否是挑战者
if (matchInfo.challenger && game.submitBy == matchInfo.challenger.id &&
currentUser.id == matchInfo.challenger.userId) {
isSubmitter = true
console.log('当前用户是提交者通过challenger判断')
}
// 检查提交者是否是被挑战者
else if (matchInfo.defender && game.submitBy == matchInfo.defender.id &&
currentUser.id == matchInfo.defender.userId) {
isSubmitter = true
console.log('当前用户是提交者通过defender判断')
}
}
}
// 如果不是提交者,且确认状态为待确认,则可以确认比分
if (!isSubmitter && game.confirmStatus === 0) {
if (!canConfirmScore) {
canConfirmScore = true
console.log('进行中状态:设置确认比分权限(对方已提交,等待确认)', {
submitBy: game.submitBy,
challengerId: matchInfo.challenger?.id,
defenderId: matchInfo.defender?.id,
myRole
})
}
}
}
}
// 如果 myRole 仍然为 null但状态是进行中尝试通过游戏中的 player1_id 和 player2_id 判断
if (!myRole && game) {
const currentUser = app.globalData.userInfo
if (currentUser) {
console.log('尝试通过游戏信息识别角色:', {
player1Id: game.player1Id,
player2Id: game.player2Id,
challengerId: matchInfo.challenger?.id,
defenderId: matchInfo.defender?.id,
currentUserId: currentUser.id,
challengerUserId: matchInfo.challenger?.userId,
defenderUserId: matchInfo.defender?.userId
})
// 通过比较 challenger/defender 的 idladder_user_id和 player1_id/player2_id 来判断
if (matchInfo.challenger && matchInfo.challenger.id == game.player1Id) {
// 如果当前用户是挑战者,且挑战者是 player1
if (currentUser.id == matchInfo.challenger.userId) {
myRole = 'challenger'
console.log('通过游戏player1Id识别为挑战者')
}
} else if (matchInfo.challenger && matchInfo.challenger.id == game.player2Id) {
// 如果当前用户是挑战者,且挑战者是 player2
if (currentUser.id == matchInfo.challenger.userId) {
myRole = 'challenger'
console.log('通过游戏player2Id识别为挑战者')
}
}
if (matchInfo.defender && matchInfo.defender.id == game.player1Id) {
// 如果当前用户是被挑战者,且被挑战者是 player1
if (currentUser.id == matchInfo.defender.userId) {
myRole = 'defender'
console.log('通过游戏player1Id识别为被挑战者')
}
} else if (matchInfo.defender && matchInfo.defender.id == game.player2Id) {
// 如果当前用户是被挑战者,且被挑战者是 player2
if (currentUser.id == matchInfo.defender.userId) {
myRole = 'defender'
console.log('通过游戏player2Id识别为被挑战者')
}
}
// 如果识别到角色,重新检查操作权限
if (myRole) {
console.log('识别到角色后,重新检查操作权限:', { myRole, gameStatus: game.status, submitBy: game.submitBy })
// 如果游戏状态为1进行中且未提交比分可以填写比分
if (game.status === 1 && !game.submitBy) {
canSubmitScore = true
console.log('识别角色后,设置填写比分权限')
}
// 如果游戏状态为2已提交且对方已提交等待我确认
else if (game.status === 2 && game.submitBy) {
// 判断当前用户是否是提交者
const isSubmitter = (myRole === 'challenger' && game.submitBy == matchInfo.challenger?.id) ||
(myRole === 'defender' && game.submitBy == matchInfo.defender?.id)
if (!isSubmitter && game.confirmStatus === 0) {
canConfirmScore = true
console.log('识别角色后,设置确认比分权限')
}
}
}
}
}
}
console.log('最终设置的操作权限:', {
canAccept,
canReject,
canSubmitScore,
canConfirmScore,
myRole,
status: matchInfo.status,
defenderInfo: matchInfo.defender ? {
id: matchInfo.defender.id,
userId: matchInfo.defender.userId,
phone: matchInfo.defender.phone,
realName: matchInfo.defender.realName
} : null,
challengerInfo: matchInfo.challenger ? {
id: matchInfo.challenger.id,
userId: matchInfo.challenger.userId,
phone: matchInfo.challenger.phone,
realName: matchInfo.challenger.realName
} : null,
currentUser: app.globalData.userInfo ? {
id: app.globalData.userInfo.id,
phone: app.globalData.userInfo.phone
} : null
})
this.setData({
matchInfo,
myRole,
canAccept,
canReject,
canSubmitScore,
canConfirmScore,
loading: false
})
// 再次检查,如果还是没有按钮,输出详细日志
if (matchInfo.status === 0 && !canAccept && !canReject) {
console.error('警告:状态为待接受但没有操作按钮!', {
myRole,
canAccept,
canReject,
matchInfoStatus: matchInfo.status,
defenderUserId: matchInfo.defender?.userId,
currentUserId: app.globalData.userInfo?.id,
defenderPhone: matchInfo.defender?.phone,
currentUserPhone: app.globalData.userInfo?.phone
})
}
} catch (e) {
this.setData({ loading: false })
console.error('加载比赛详情失败:', e)
console.error('错误详情:', e.message, e.data, e)
wx.showToast({ title: '加载失败: ' + (e.message || '未知错误'), icon: 'none', duration: 3000 })
}
},
// 处理挑战请求从WebSocket调用
handleChallengeRequest(challengeData) {
// 如果当前页面是挑战赛详情且是同一个比赛,显示弹框
if (this.data.matchId == challengeData.matchId) {
this.showChallengeModal(challengeData)
} else {
// 否则跳转到挑战赛详情页面
wx.navigateTo({
url: `/pages/match/challenge-detail/index?id=${challengeData.matchId}`
})
}
},
// 显示挑战弹框
showChallengeModal(challengeData) {
wx.showModal({
title: '收到挑战',
content: `${challengeData.challenger.realName}(Lv${challengeData.challenger.level}, 战力${challengeData.challenger.powerScore}) 向你发起挑战`,
confirmText: '接受',
cancelText: '拒绝',
success: (res) => {
this.respondChallenge(res.confirm)
}
})
},
// 响应挑战
async respondChallenge(accept) {
wx.showLoading({ title: accept ? '接受中...' : '拒绝中...' })
try {
await app.request('/api/match/challenge/respond', {
match_id: this.data.matchId,
accept: accept
}, 'POST')
wx.hideLoading()
wx.showToast({
title: accept ? '已接受挑战' : '已拒绝挑战',
icon: 'success'
})
// 刷新数据
setTimeout(() => {
this.loadMatchDetail()
}, 1500)
} catch (e) {
wx.hideLoading()
console.error('响应挑战失败:', e)
wx.showToast({ title: '操作失败', icon: 'none' })
}
},
// 接受挑战
acceptChallenge() {
this.respondChallenge(true)
},
// 拒绝挑战
rejectChallenge() {
wx.showModal({
title: '确认拒绝',
content: '确定要拒绝这个挑战吗?',
success: (res) => {
if (res.confirm) {
this.respondChallenge(false)
}
}
})
},
// 打开填写比分弹框
openScoreModal() {
this.setData({
showScoreModal: true,
myScore: '',
opponentScore: ''
})
},
// 关闭填写比分弹框
closeScoreModal() {
this.setData({ showScoreModal: false })
},
// 输入我的比分
onMyScoreInput(e) {
this.setData({ myScore: e.detail.value })
},
// 输入对手比分
onOpponentScoreInput(e) {
this.setData({ opponentScore: e.detail.value })
},
// 提交比分
async submitScore() {
const { myScore, opponentScore } = this.data
if (!myScore || !opponentScore) {
wx.showToast({ title: '请填写完整比分', icon: 'none' })
return
}
const myScoreNum = parseInt(myScore)
const opponentScoreNum = parseInt(opponentScore)
if (isNaN(myScoreNum) || isNaN(opponentScoreNum)) {
wx.showToast({ title: '请输入有效数字', icon: 'none' })
return
}
if (myScoreNum === opponentScoreNum) {
wx.showToast({ title: '比分不能相同', icon: 'none' })
return
}
wx.showLoading({ title: '提交中...' })
try {
await app.request('/api/match/challenge/submit-score', {
match_id: this.data.matchId,
my_score: myScoreNum,
opponent_score: opponentScoreNum
}, 'POST')
wx.hideLoading()
wx.showToast({ title: '比分已提交,等待对方确认', icon: 'success' })
this.closeScoreModal()
// 刷新数据
setTimeout(() => {
this.loadMatchDetail()
}, 1500)
} catch (e) {
wx.hideLoading()
console.error('提交比分失败:', e)
wx.showToast({ title: e.message || '提交失败', icon: 'none' })
}
},
// 确认比分
async confirmScore(confirm) {
const game = this.data.matchInfo.games?.[0]
if (!game) {
wx.showToast({ title: '比赛信息错误', icon: 'none' })
return
}
wx.showLoading({ title: '处理中...' })
try {
await app.request('/api/match/challenge/confirm-score', {
game_id: game.id,
confirm: confirm
}, 'POST')
wx.hideLoading()
wx.showToast({
title: confirm ? '已确认比分' : '已标记争议',
icon: 'success'
})
// 刷新数据
setTimeout(() => {
this.loadMatchDetail()
}, 1500)
} catch (e) {
wx.hideLoading()
console.error('确认比分失败:', e)
wx.showToast({ title: '操作失败', icon: 'none' })
}
},
// 确认比分按钮
confirmScoreBtn() {
const game = this.data.matchInfo.games?.[0]
if (!game) {
wx.showToast({ title: '比赛信息错误', icon: 'none' })
return
}
// 根据当前用户角色显示正确的比分信息
let myScore = 0
let opponentScore = 0
let myName = ''
let opponentName = ''
if (this.data.myRole === 'challenger') {
// 挑战者是 player1 还是 player2
if (this.data.matchInfo.challenger && this.data.matchInfo.challenger.id == game.player1Id) {
myScore = game.player1Score || 0
opponentScore = game.player2Score || 0
myName = this.data.matchInfo.challenger.realName || '挑战者'
opponentName = this.data.matchInfo.defender?.realName || '被挑战者'
} else if (this.data.matchInfo.challenger && this.data.matchInfo.challenger.id == game.player2Id) {
myScore = game.player2Score || 0
opponentScore = game.player1Score || 0
myName = this.data.matchInfo.challenger.realName || '挑战者'
opponentName = this.data.matchInfo.defender?.realName || '被挑战者'
} else {
// 如果无法确定,使用默认显示
myScore = game.player1Score || 0
opponentScore = game.player2Score || 0
}
} else if (this.data.myRole === 'defender') {
// 被挑战者是 player1 还是 player2
if (this.data.matchInfo.defender && this.data.matchInfo.defender.id == game.player1Id) {
myScore = game.player1Score || 0
opponentScore = game.player2Score || 0
myName = this.data.matchInfo.defender.realName || '被挑战者'
opponentName = this.data.matchInfo.challenger?.realName || '挑战者'
} else if (this.data.matchInfo.defender && this.data.matchInfo.defender.id == game.player2Id) {
myScore = game.player2Score || 0
opponentScore = game.player1Score || 0
myName = this.data.matchInfo.defender.realName || '被挑战者'
opponentName = this.data.matchInfo.challenger?.realName || '挑战者'
} else {
// 如果无法确定,使用默认显示
myScore = game.player1Score || 0
opponentScore = game.player2Score || 0
}
} else {
// 如果角色未知,使用默认显示
myScore = game.player1Score || 0
opponentScore = game.player2Score || 0
}
wx.showModal({
title: '确认比分',
content: `对方提交的比分为:\n${opponentName}: ${opponentScore}\n${myName}: ${myScore}\n\n请确认此比分是否正确?`,
confirmText: '确认',
cancelText: '有争议',
success: (res) => {
if (res.confirm) {
this.confirmScore(true)
} else {
// 有争议
wx.showModal({
title: '确认争议',
content: '确定要标记为有争议吗?标记后需要重新比赛。',
confirmText: '确定',
cancelText: '取消',
success: (res2) => {
if (res2.confirm) {
this.confirmScore(false)
}
}
})
}
}
})
},
// 阻止事件冒泡
stopPropagation() {
// 空函数,用于阻止事件冒泡
}
})

View File

@ -0,0 +1,5 @@
{
"navigationBarTitleText": "挑战赛详情",
"enablePullDownRefresh": true,
"backgroundColor": "#f5f7fa"
}

View File

@ -0,0 +1,121 @@
<!--挑战赛详情页面-->
<view class="page-container" catchtap="stopPropagation">
<!-- 加载中 -->
<view class="loading-container" wx:if="{{loading}}">
<text>加载中...</text>
</view>
<!-- 比赛信息 -->
<view class="match-info" wx:if="{{!loading && matchInfo}}">
<!-- 比赛标题 -->
<view class="match-header">
<text class="match-title">{{matchInfo.name}}</text>
<view class="match-status status-{{matchInfo.status}}">
<text wx:if="{{matchInfo.status === 0}}">待接受</text>
<text wx:elif="{{matchInfo.status === 1}}">进行中</text>
<text wx:elif="{{matchInfo.status === 2}}">已结束</text>
<text wx:elif="{{matchInfo.status === 3}}">已取消</text>
</view>
</view>
<!-- 对手信息 -->
<view class="opponent-section">
<view class="opponent-card" wx:if="{{matchInfo.challenger}}">
<view class="opponent-label">挑战者</view>
<view class="opponent-info">
<image class="opponent-avatar" src="{{matchInfo.challenger.avatar || '/images/avatar-default.svg'}}" mode="aspectFill"></image>
<view class="opponent-details">
<text class="opponent-name">{{matchInfo.challenger.realName}}</text>
<text class="opponent-level">Lv{{matchInfo.challenger.level}} · 战力{{matchInfo.challenger.powerScore}}</text>
</view>
</view>
</view>
<view class="vs-divider">
<text>VS</text>
</view>
<view class="opponent-card" wx:if="{{matchInfo.defender}}">
<view class="opponent-label">被挑战者</view>
<view class="opponent-info">
<image class="opponent-avatar" src="{{matchInfo.defender.avatar || '/images/avatar-default.svg'}}" mode="aspectFill"></image>
<view class="opponent-details">
<text class="opponent-name">{{matchInfo.defender.realName}}</text>
<text class="opponent-level">Lv{{matchInfo.defender.level}} · 战力{{matchInfo.defender.powerScore}}</text>
</view>
</view>
</view>
</view>
<!-- 比赛进度 -->
<view class="match-progress" wx:if="{{matchInfo.games && matchInfo.games.length > 0}}">
<view class="progress-title">比赛进度</view>
<view class="game-item" wx:for="{{matchInfo.games}}" wx:key="id">
<view class="game-score">
<text class="score-label">比分:</text>
<text class="score-value">{{item.player1Score !== null && item.player1Score !== undefined ? item.player1Score : 0}} : {{item.player2Score !== null && item.player2Score !== undefined ? item.player2Score : 0}}</text>
</view>
<view class="game-status">
<text wx:if="{{item.status === 0}}">未开始</text>
<text wx:elif="{{item.status === 1}}">进行中</text>
<text wx:elif="{{item.status === 2 && item.confirmStatus === 0}}">等待确认</text>
<text wx:elif="{{item.status === 2 && item.confirmStatus === 1}}">已确认</text>
<text wx:elif="{{item.status === 2 && item.confirmStatus === 2}}">有争议</text>
</view>
<!-- 等待确认时显示提示信息 -->
<view class="confirm-tip" wx:if="{{item.status === 2 && item.confirmStatus === 0 && canConfirmScore}}">
<text class="tip-text">对方已提交比分,请确认</text>
</view>
</view>
</view>
<!-- 操作按钮 -->
<view class="action-buttons" wx:if="{{canAccept || canReject || canSubmitScore || canConfirmScore}}">
<!-- 待接受状态:显示接受/拒绝按钮 -->
<block wx:if="{{canAccept || canReject}}">
<button class="action-btn accept-btn" wx:if="{{canAccept}}" bindtap="acceptChallenge">接受挑战</button>
<button class="action-btn reject-btn" wx:if="{{canReject}}" bindtap="rejectChallenge">拒绝挑战</button>
</block>
<!-- 进行中状态:显示填写比分按钮 -->
<button class="action-btn submit-btn" wx:if="{{canSubmitScore}}" bindtap="openScoreModal">填写比分</button>
<!-- 等待确认状态:显示确认比分按钮 -->
<button class="action-btn confirm-btn" wx:if="{{canConfirmScore}}" bindtap="confirmScoreBtn">确认比分</button>
</view>
<!-- 无操作权限提示 -->
<view class="no-action-tip" wx:if="{{!canAccept && !canReject && !canSubmitScore && !canConfirmScore && matchInfo.status === 0 && myRole === 'challenger'}}">
<text class="tip-text">等待对方接受挑战...</text>
</view>
<!-- 被挑战者但无按钮时的提示 -->
<view class="no-action-tip" wx:if="{{!canAccept && !canReject && !canSubmitScore && !canConfirmScore && matchInfo.status === 0 && myRole === 'defender'}}">
<text class="tip-text">无法操作,请联系管理员</text>
</view>
</view>
</view>
<!-- 填写比分弹框 -->
<view class="score-modal" wx:if="{{showScoreModal}}" bindtap="closeScoreModal">
<view class="score-modal-content" catchtap="stopPropagation">
<view class="modal-header">
<text class="modal-title">填写比分</text>
<text class="modal-close" bindtap="closeScoreModal">×</text>
</view>
<view class="modal-body">
<view class="score-input-group">
<text class="input-label">我的比分</text>
<input class="score-input" type="number" placeholder="请输入比分" value="{{myScore}}" bindinput="onMyScoreInput" />
</view>
<view class="score-input-group">
<text class="input-label">对手比分</text>
<input class="score-input" type="number" placeholder="请输入比分" value="{{opponentScore}}" bindinput="onOpponentScoreInput" />
</view>
</view>
<view class="modal-footer">
<button class="modal-btn cancel-btn" bindtap="closeScoreModal">取消</button>
<button class="modal-btn submit-btn" bindtap="submitScore">提交</button>
</view>
</view>
</view>

View File

@ -0,0 +1,383 @@
.page-container {
min-height: 100vh;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
padding: 20rpx;
}
.loading-container {
display: flex;
justify-content: center;
align-items: center;
height: 60vh;
color: #666;
}
.match-info {
background: #fff;
border-radius: 24rpx;
padding: 40rpx;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1);
}
.match-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 40rpx;
padding-bottom: 30rpx;
border-bottom: 2rpx solid #f0f0f0;
}
.match-title {
font-size: 36rpx;
font-weight: 600;
color: #333;
}
.match-status {
padding: 8rpx 20rpx;
border-radius: 20rpx;
font-size: 24rpx;
}
.status-0 {
background: #fff3cd;
color: #856404;
}
.status-1 {
background: #d1ecf1;
color: #0c5460;
}
.status-2 {
background: #d4edda;
color: #155724;
}
.status-3 {
background: #f8d7da;
color: #721c24;
}
.opponent-section {
margin-bottom: 40rpx;
}
.opponent-card {
margin-bottom: 30rpx;
}
.opponent-label {
font-size: 24rpx;
color: #999;
margin-bottom: 20rpx;
}
.opponent-info {
display: flex;
align-items: center;
gap: 24rpx;
}
.opponent-avatar {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
border: 4rpx solid #e0e0e0;
}
.opponent-details {
flex: 1;
display: flex;
flex-direction: column;
gap: 8rpx;
}
.opponent-name {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.opponent-level {
font-size: 24rpx;
color: #666;
}
.vs-divider {
text-align: center;
margin: 30rpx 0;
font-size: 32rpx;
font-weight: 600;
color: #999;
}
.match-progress {
margin-bottom: 40rpx;
padding-top: 30rpx;
border-top: 2rpx solid #f0f0f0;
}
.progress-title {
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 20rpx;
}
.game-item {
background: #f8f9fa;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 16rpx;
}
.game-score {
display: flex;
align-items: center;
margin-bottom: 12rpx;
}
.score-label {
font-size: 24rpx;
color: #666;
}
.score-value {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-left: 12rpx;
}
.game-status {
font-size: 24rpx;
color: #999;
}
.confirm-tip {
margin-top: 16rpx;
padding: 16rpx 20rpx;
background: linear-gradient(135deg, #fff5f0 0%, #ffe8d6 100%);
border-radius: 12rpx;
border-left: 4rpx solid #ff6b35;
box-shadow: 0 2rpx 8rpx rgba(255, 107, 53, 0.1);
}
.confirm-tip .tip-text {
font-size: 26rpx;
color: #d84315;
font-weight: 500;
}
.action-buttons {
display: flex;
gap: 20rpx;
margin-top: 40rpx;
}
.no-action-tip {
margin-top: 40rpx;
padding: 30rpx;
text-align: center;
background: #f8f9fa;
border-radius: 16rpx;
}
.tip-text {
font-size: 28rpx;
color: #999;
}
.action-btn {
flex: 1;
height: 88rpx;
border-radius: 44rpx;
font-size: 32rpx;
font-weight: 600;
border: none;
display: flex;
align-items: center;
justify-content: center;
}
.accept-btn {
background: linear-gradient(135deg, #ff6b35 0%, #ff8c42 100%);
color: #fff;
box-shadow: 0 4rpx 16rpx rgba(255, 107, 53, 0.3);
}
.accept-btn:active {
background: linear-gradient(135deg, #e55a2b 0%, #e67e2f 100%);
box-shadow: 0 2rpx 8rpx rgba(255, 107, 53, 0.4);
}
.reject-btn {
background: #f5f5f5;
color: #666;
}
.submit-btn {
background: linear-gradient(135deg, #ff6b35 0%, #ff8c42 100%);
color: #fff;
box-shadow: 0 4rpx 16rpx rgba(255, 107, 53, 0.3);
}
.submit-btn:active {
background: linear-gradient(135deg, #e55a2b 0%, #e67e2f 100%);
box-shadow: 0 2rpx 8rpx rgba(255, 107, 53, 0.4);
}
.confirm-btn {
background: linear-gradient(135deg, #ff6b35 0%, #ff8c42 100%);
color: #fff;
box-shadow: 0 4rpx 16rpx rgba(255, 107, 53, 0.3);
}
.confirm-btn:active {
background: linear-gradient(135deg, #e55a2b 0%, #e67e2f 100%);
box-shadow: 0 2rpx 8rpx rgba(255, 107, 53, 0.4);
}
/* 填写比分弹框 */
.score-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.score-modal-content {
width: 600rpx;
max-width: 90%;
background: #fff;
border-radius: 24rpx;
overflow: hidden;
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.15);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx 40rpx;
border-bottom: 2rpx solid rgba(255, 255, 255, 0.2);
background: linear-gradient(135deg, #ff6b35 0%, #ff8c42 100%);
}
.modal-header .modal-title {
color: #fff;
}
.modal-header .modal-close {
color: #fff;
opacity: 0.9;
font-size: 40rpx;
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
transition: all 0.3s ease;
}
.modal-header .modal-close:active {
background: rgba(255, 255, 255, 0.3);
transform: scale(0.95);
}
.modal-body {
padding: 40rpx;
}
.score-input-group {
margin-bottom: 32rpx;
}
.score-input-group:last-child {
margin-bottom: 0;
}
.input-label {
display: block;
font-size: 28rpx;
color: #333;
font-weight: 500;
margin-bottom: 12rpx;
}
.score-input {
width: 100%;
height: 88rpx;
background: #fff;
border-radius: 12rpx;
padding: 0 24rpx;
font-size: 32rpx;
color: #333;
border: 2rpx solid #e0e0e0;
box-sizing: border-box;
transition: all 0.3s ease;
}
.score-input:focus {
border-color: #ff6b35;
background: #fff5f0;
}
.modal-footer {
display: flex;
gap: 20rpx;
padding: 32rpx 40rpx;
border-top: 2rpx solid #f0f0f0;
background: #fafafa;
}
.modal-btn {
flex: 1;
height: 88rpx;
border-radius: 44rpx;
font-size: 32rpx;
font-weight: 600;
border: none;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
}
.modal-btn:active {
transform: scale(0.98);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.15);
}
.cancel-btn {
background: #fff;
color: #666;
border: 2rpx solid #e0e0e0;
}
.cancel-btn:active {
background: #f5f5f5;
}
.modal-btn.submit-btn {
background: linear-gradient(135deg, #ff6b35 0%, #ff8c42 100%);
color: #fff;
box-shadow: 0 4rpx 16rpx rgba(255, 107, 53, 0.3);
}
.modal-btn.submit-btn:active {
background: linear-gradient(135deg, #e55a2b 0%, #e67e2f 100%);
box-shadow: 0 2rpx 8rpx rgba(255, 107, 53, 0.4);
}

View File

@ -172,16 +172,27 @@ Page({
wx.showLoading({ title: '发起挑战中...' }) wx.showLoading({ title: '发起挑战中...' })
try { try {
await app.request('/api/match/challenge/create', { const res = await app.request('/api/match/challenge/create', {
store_id: this.data.currentStore.storeId, store_id: this.data.currentStore.storeId,
target_member_code: memberCode target_member_code: memberCode
}, 'POST') }, 'POST')
wx.hideLoading() wx.hideLoading()
wx.showToast({ title: '挑战已发起', icon: 'success' }) wx.showToast({ title: '挑战已发起', icon: 'success' })
// 跳转到挑战赛详情页面
if (res.data && res.data.matchId) {
setTimeout(() => {
wx.navigateTo({
url: `/pages/match/challenge-detail/index?id=${res.data.matchId}`
})
}, 1500)
}
} catch (e) { } catch (e) {
wx.hideLoading() wx.hideLoading()
console.error('发起挑战失败:', e) console.error('发起挑战失败:', e)
const errorMsg = e.message || e.data?.message || '发起挑战失败'
wx.showToast({ title: errorMsg, icon: 'none', duration: 2000 })
} }
}, },
@ -231,9 +242,9 @@ Page({
goToMatchDetail(e) { goToMatchDetail(e) {
const match = e.currentTarget.dataset.match const match = e.currentTarget.dataset.match
if (match.type === 1) { if (match.type === 1) {
// 挑战赛详情 - 暂时跳转到历史记录页 // 挑战赛详情
wx.navigateTo({ wx.navigateTo({
url: `/pages/match/history/index` url: `/pages/match/challenge-detail/index?id=${match.id}`
}) })
} else { } else {
// 排位赛详情 // 排位赛详情

View File

@ -16,32 +16,17 @@
left: 0; left: 0;
right: 0; right: 0;
height: 420rpx; height: 420rpx;
background: linear-gradient(135deg, #FF8A65 0%, #FF6B35 50%, #F4511E 100%); background: transparent;
border-radius: 0 0 60rpx 60rpx;
pointer-events: none; pointer-events: none;
overflow: hidden; overflow: hidden;
} }
.hero-bg::before { .hero-bg::before {
content: ''; display: none;
position: absolute;
top: -100rpx;
right: -80rpx;
width: 300rpx;
height: 300rpx;
background: rgba(255, 255, 255, 0.15);
border-radius: 50%;
} }
.hero-bg::after { .hero-bg::after {
content: ''; display: none;
position: absolute;
bottom: -60rpx;
left: -60rpx;
width: 200rpx;
height: 200rpx;
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
} }
.hero-pattern { .hero-pattern {
@ -70,23 +55,25 @@
/* 页面标题 */ /* 页面标题 */
.page-header { .page-header {
text-align: center; text-align: center;
padding: 20rpx 0 30rpx; padding: 32rpx 0 40rpx;
margin-bottom: 8rpx;
} }
.page-title { .page-title {
display: block; display: block;
font-size: 48rpx; font-size: 52rpx;
font-weight: 800; font-weight: 700;
color: #fff; color: #1a1a1a;
margin-bottom: 10rpx; margin-bottom: 12rpx;
text-shadow: 0 4rpx 10rpx rgba(0, 0, 0, 0.15); letter-spacing: 1rpx;
letter-spacing: 4rpx;
} }
.page-subtitle { .page-subtitle {
display: block; display: block;
font-size: 26rpx; font-size: 26rpx;
color: rgba(255, 255, 255, 0.9); color: #999;
font-weight: 400;
letter-spacing: 0.5rpx;
} }
/* 当前门店栏 */ /* 当前门店栏 */
@ -95,13 +82,13 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 10rpx; gap: 10rpx;
padding: 16rpx 24rpx; padding: 20rpx 24rpx;
background: rgba(255, 255, 255, 0.95); background: var(--bg-white);
border-radius: 50rpx; border-radius: 16rpx;
box-shadow: 0 8rpx 24rpx rgba(255, 107, 53, 0.2); box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
margin: 0 auto 24rpx; margin: 0 auto 24rpx;
width: fit-content; width: fit-content;
backdrop-filter: blur(10px); border: 1rpx solid rgba(0, 0, 0, 0.04);
} }
.store-icon { .store-icon {

View File

@ -51,10 +51,31 @@ Page({
pageSize: this.data.pageSize pageSize: this.data.pageSize
}) })
const matches = (res.data.list || []).map(match => ({ const matches = (res.data.list || []).map(match => {
...match, // 确保 powerChange 是数字类型,移除可能存在的加号和其他非数字字符
confirmedAt: util.formatDate(match.confirmedAt) let powerChange = match.powerChange
})) if (powerChange != null && powerChange !== undefined) {
// 如果是字符串,移除所有加号、空格等非数字字符(保留负号)
if (typeof powerChange === 'string') {
// 保留负号,移除所有加号和其他字符
const cleaned = powerChange.replace(/\+/g, '').trim()
powerChange = parseFloat(cleaned) || 0
}
// 确保是数字类型
powerChange = Number(powerChange)
// 如果是 NaN设为 0
if (isNaN(powerChange)) {
powerChange = 0
}
} else {
powerChange = 0
}
return {
...match,
powerChange: powerChange,
confirmedAt: util.formatDate(match.confirmedAt)
}
})
this.setData({ this.setData({
matches: this.data.page === 1 ? matches : [...this.data.matches, ...matches], matches: this.data.page === 1 ? matches : [...this.data.matches, ...matches],

View File

@ -7,15 +7,17 @@
<text class="match-type">{{item.matchType === 1 ? '挑战赛' : '排位赛'}}</text> <text class="match-type">{{item.matchType === 1 ? '挑战赛' : '排位赛'}}</text>
</view> </view>
<view class="match-content"> <view class="match-content">
<view class="result {{item.isWin ? 'win' : 'lose'}}"> <view class="content-row">
{{item.isWin ? '胜' : '负'}} <view class="result {{item.isWin ? 'win' : 'lose'}}">
</view> {{item.isWin ? '胜' : '负'}}
<view class="score-info"> </view>
<text class="opponent">vs {{item.opponentName}}</text> <view class="score-info">
<text class="score">{{item.myScore}} : {{item.opponentScore}}</text> <text class="opponent">vs {{item.opponentName}}</text>
</view> <text class="score">{{item.myScore}} : {{item.opponentScore}}</text>
<view class="power-change {{item.powerChange > 0 ? 'positive' : 'negative'}}"> </view>
{{item.powerChange > 0 ? '+' : ''}}{{item.powerChange}} <view class="power-change {{item.powerChange > 0 ? 'positive' : 'negative'}}">
<text wx:if="{{item.powerChange > 0}}">+</text><text>{{item.powerChange}}</text>
</view>
</view> </view>
</view> </view>
<view class="match-footer"> <view class="match-footer">

View File

@ -1,364 +1,166 @@
/* ========================================== /* ==========================================
比赛记录页面 - 浅色高级感设计 比赛记录页面 - 橙色主题优化
========================================== */ ========================================== */
.page-container { .container {
min-height: 100vh; min-height: 100vh;
background: var(--bg-page); background: #f5f5f5;
position: relative;
}
/* 顶部装饰背景 */
.hero-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 300rpx;
background: linear-gradient(180deg, #FFF5F0 0%, var(--bg-page) 100%);
pointer-events: none;
}
.hero-pattern {
position: absolute;
top: -60rpx;
right: -40rpx;
width: 240rpx;
height: 240rpx;
background: radial-gradient(circle, rgba(255, 107, 53, 0.08) 0%, transparent 70%);
border-radius: 50%;
}
/* 页面标题 */
.page-header {
position: relative;
z-index: 1;
text-align: center;
padding: 32rpx 24rpx 24rpx;
animation: fadeInUp 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
.page-title {
display: block;
font-size: 40rpx;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 8rpx;
letter-spacing: 2rpx;
}
.page-subtitle {
display: block;
font-size: 26rpx;
color: var(--text-muted);
}
/* 筛选标签栏 */
.filter-bar {
position: relative;
z-index: 1;
display: flex;
gap: 16rpx;
padding: 0 24rpx 24rpx;
animation: fadeInUp 0.5s cubic-bezier(0.4, 0, 0.2, 1) 0.1s backwards;
}
.filter-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 20rpx; padding: 20rpx;
background: var(--bg-white); }
border-radius: var(--radius-full);
box-shadow: var(--shadow-sm); .history-list {
font-size: 26rpx; display: flex;
color: var(--text-secondary); flex-direction: column;
gap: 20rpx;
}
.match-item {
background: #fff;
border-radius: 20rpx;
overflow: hidden;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.filter-item.active { .match-item:active {
background: var(--primary-gradient);
color: var(--text-white);
box-shadow: var(--shadow-primary);
}
.filter-item:active {
transform: scale(0.96);
}
/* 比赛列表 */
.match-list {
position: relative;
z-index: 1;
padding: 0 24rpx;
}
.match-card {
background: var(--bg-white);
border-radius: var(--radius-lg);
margin-bottom: 20rpx;
box-shadow: var(--shadow-card);
overflow: hidden;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
animation: fadeInUp 0.5s cubic-bezier(0.4, 0, 0.2, 1) backwards;
}
.match-card:nth-child(1) { animation-delay: 0.15s; }
.match-card:nth-child(2) { animation-delay: 0.2s; }
.match-card:nth-child(3) { animation-delay: 0.25s; }
.match-card:nth-child(4) { animation-delay: 0.3s; }
.match-card:nth-child(5) { animation-delay: 0.35s; }
.match-card:active {
transform: scale(0.98); transform: scale(0.98);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.12);
} }
.match-card.win {
border-left: 6rpx solid var(--accent);
}
.match-card.lose {
border-left: 6rpx solid #FF6B6B;
}
/* 比赛头部 */
.match-header { .match-header {
display: flex; display: flex;
align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 20rpx 24rpx; align-items: center;
background: linear-gradient(90deg, #FAFBFC, var(--bg-white)); padding: 24rpx 28rpx;
border-bottom: 1rpx solid var(--border-soft); background: linear-gradient(135deg, #ff6b35 0%, #ff8c42 100%);
border-bottom: 2rpx solid rgba(255, 255, 255, 0.2);
}
.match-name {
font-size: 32rpx;
font-weight: 600;
color: #fff;
flex: 1;
} }
.match-type { .match-type {
padding: 6rpx 16rpx;
background: rgba(255, 255, 255, 0.25);
border-radius: 20rpx;
font-size: 22rpx;
color: #fff;
font-weight: 500;
}
.match-content {
padding: 28rpx;
}
.content-row {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 24rpx;
}
.result {
display: inline-block;
width: 60rpx;
height: 60rpx;
line-height: 60rpx;
text-align: center;
border-radius: 50%;
font-size: 28rpx;
font-weight: 600;
}
.result.win {
background: linear-gradient(135deg, #4caf50 0%, #66bb6a 100%);
color: #fff;
box-shadow: 0 4rpx 12rpx rgba(76, 175, 80, 0.3);
}
.result.lose {
background: linear-gradient(135deg, #f44336 0%, #ef5350 100%);
color: #fff;
box-shadow: 0 4rpx 12rpx rgba(244, 67, 54, 0.3);
}
.score-info {
display: flex;
flex-direction: column;
gap: 8rpx; gap: 8rpx;
flex: 1;
} }
.type-badge { .opponent {
padding: 6rpx 14rpx; font-size: 28rpx;
border-radius: var(--radius-full); color: #666;
font-size: 22rpx; font-weight: 500;
}
.score {
font-size: 40rpx;
font-weight: 700;
color: #333;
letter-spacing: 4rpx;
}
.power-change {
font-size: 32rpx;
font-weight: 600; font-weight: 600;
padding: 8rpx 16rpx;
border-radius: 12rpx;
text-align: center;
min-width: 100rpx;
} }
.type-badge.ladder { .power-change.positive {
background: var(--primary-soft); background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%);
color: var(--primary); color: #2e7d32;
} }
.type-badge.friendly { .power-change.negative {
background: var(--accent-light); background: linear-gradient(135deg, #ffebee 0%, #ffcdd2 100%);
color: var(--accent); color: #c62828;
} }
.result-badge { .match-footer {
padding: 6rpx 14rpx; padding: 20rpx 28rpx;
border-radius: var(--radius-full); background: #fafafa;
font-size: 22rpx; border-top: 1rpx solid #f0f0f0;
font-weight: 600;
}
.result-badge.win {
background: var(--accent-light);
color: var(--accent);
}
.result-badge.lose {
background: #FFF1F0;
color: #FF4D4F;
} }
.match-time { .match-time {
font-size: 24rpx; font-size: 24rpx;
color: var(--text-muted); color: #999;
} }
/* 比赛内容 */
.match-content {
padding: 24rpx;
}
.match-players {
display: flex;
align-items: center;
justify-content: space-between;
}
.player-card {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
}
.player-card.self {
position: relative;
}
.player-card.self::before {
content: '我';
position: absolute;
top: -8rpx;
right: 20%;
padding: 2rpx 10rpx;
background: var(--primary);
color: #FFF;
font-size: 18rpx;
border-radius: var(--radius-full);
}
.player-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
margin-bottom: 12rpx;
border: 3rpx solid var(--bg-white);
box-shadow: var(--shadow-sm);
}
.player-name {
font-size: 26rpx;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 6rpx;
}
.player-level {
padding: 4rpx 12rpx;
border-radius: var(--radius-full);
font-size: 20rpx;
font-weight: 600;
}
.player-level.lv1 { background: #E8F5E9; color: #2E7D32; }
.player-level.lv2 { background: #E3F2FD; color: #1565C0; }
.player-level.lv3 { background: #FFF3E0; color: #E65100; }
.player-level.lv4 { background: #FCE4EC; color: #C2185B; }
.player-level.lv5 { background: #F3E5F5; color: #7B1FA2; }
/* 比分区域 */
.score-area {
display: flex;
flex-direction: column;
align-items: center;
padding: 0 16rpx;
}
.vs-text {
font-size: 24rpx;
color: var(--text-hint);
font-weight: 600;
}
.score-display {
display: flex;
align-items: center;
gap: 12rpx;
margin-top: 8rpx;
}
.score-value {
font-size: 44rpx;
font-weight: 700;
color: var(--text-primary);
}
.score-colon {
font-size: 32rpx;
color: var(--text-hint);
}
/* 比赛详情 */
.match-details {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 20rpx;
padding-top: 20rpx;
border-top: 1rpx solid var(--border-soft);
}
.detail-item {
display: flex;
align-items: center;
gap: 8rpx;
}
.detail-label {
font-size: 24rpx;
color: var(--text-muted);
}
.detail-value {
font-size: 26rpx;
font-weight: 600;
}
.detail-value.positive {
color: var(--accent);
}
.detail-value.positive::before {
content: '+';
}
.detail-value.negative {
color: #FF4D4F;
}
/* 空状态 */
.empty-state { .empty-state {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
padding: 100rpx 48rpx; justify-content: center;
animation: fadeInUp 0.5s cubic-bezier(0.4, 0, 0.2, 1) 0.2s backwards; padding: 120rpx 40rpx;
min-height: 60vh;
} }
.empty-icon { .empty-state image {
width: 180rpx; width: 200rpx;
height: 180rpx; height: 200rpx;
opacity: 0.6;
margin-bottom: 32rpx; margin-bottom: 32rpx;
opacity: 0.7;
} }
.empty-title { .empty-state text {
font-size: 30rpx; font-size: 28rpx;
font-weight: 600; color: #999;
color: var(--text-secondary);
margin-bottom: 12rpx;
} }
.empty-desc {
font-size: 26rpx;
color: var(--text-muted);
text-align: center;
line-height: 1.6;
}
/* 加载状态 */
.loading-state { .loading-state {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 48rpx; padding: 40rpx;
color: var(--text-muted); color: #999;
font-size: 26rpx; font-size: 28rpx;
}
/* 底部安全区域 */
.safe-bottom {
height: 80rpx;
}
/* 加载更多 */
.load-more {
text-align: center;
padding: 32rpx;
color: var(--text-muted);
font-size: 26rpx;
} }

View File

@ -16,32 +16,17 @@
left: 0; left: 0;
right: 0; right: 0;
height: 380rpx; height: 380rpx;
background: linear-gradient(135deg, #FFB74D 0%, #FF9800 50%, #F57C00 100%); background: transparent;
border-radius: 0 0 60rpx 60rpx;
pointer-events: none; pointer-events: none;
overflow: hidden; overflow: hidden;
} }
.hero-bg::before { .hero-bg::before {
content: ''; display: none;
position: absolute;
top: -80rpx;
right: -60rpx;
width: 260rpx;
height: 260rpx;
background: rgba(255, 255, 255, 0.15);
border-radius: 50%;
} }
.hero-bg::after { .hero-bg::after {
content: ''; display: none;
position: absolute;
bottom: -40rpx;
left: -40rpx;
width: 180rpx;
height: 180rpx;
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
} }
.hero-pattern { .hero-pattern {
@ -70,7 +55,9 @@
/* 比赛头部 */ /* 比赛头部 */
.match-header { .match-header {
text-align: center; text-align: center;
padding: 32rpx 0 40rpx; padding: 48rpx 0 40rpx;
border-bottom: 1rpx solid rgba(0, 0, 0, 0.04);
margin-bottom: 24rpx;
} }
.match-badge { .match-badge {
@ -80,12 +67,11 @@
.match-title { .match-title {
display: block; display: block;
font-size: 44rpx; font-size: 48rpx;
font-weight: 800; font-weight: 700;
color: #fff; color: #1a1a1a;
margin-bottom: 16rpx; margin-bottom: 16rpx;
text-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15); letter-spacing: 1rpx;
letter-spacing: 4rpx;
} }
.match-status { .match-status {
@ -93,19 +79,18 @@
align-items: center; align-items: center;
gap: 8rpx; gap: 8rpx;
padding: 10rpx 24rpx; padding: 10rpx 24rpx;
background: rgba(255, 255, 255, 0.25); background: var(--bg-soft);
border-radius: 50rpx; border-radius: 50rpx;
font-size: 26rpx; font-size: 26rpx;
font-weight: 600; font-weight: 600;
color: #fff; color: #666;
backdrop-filter: blur(10px);
} }
.status-dot { .status-dot {
width: 12rpx; width: 12rpx;
height: 12rpx; height: 12rpx;
border-radius: 50%; border-radius: 50%;
background: #fff; background: #ff6b35;
} }
.match-status.status-1 .status-dot { .match-status.status-1 .status-dot {

View File

@ -12,19 +12,13 @@
/* 顶部装饰背景 */ /* 顶部装饰背景 */
.hero-section { .hero-section {
position: relative; position: relative;
padding: 32rpx 24rpx; padding: 48rpx 24rpx 32rpx;
background: linear-gradient(180deg, #FFF5F0 0%, var(--bg-page) 100%); background: transparent;
border-bottom: 1rpx solid rgba(0, 0, 0, 0.04);
} }
.hero-pattern { .hero-pattern {
position: absolute; display: none;
top: -60rpx;
right: -60rpx;
width: 280rpx;
height: 280rpx;
background: radial-gradient(circle, rgba(255, 107, 53, 0.1) 0%, transparent 70%);
border-radius: 50%;
pointer-events: none;
} }
/* 积分信息卡片 */ /* 积分信息卡片 */
@ -34,10 +28,11 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 28rpx 24rpx; padding: 32rpx 28rpx;
background: var(--bg-white); background: var(--bg-white);
border-radius: var(--radius-xl); border-radius: 20rpx;
box-shadow: var(--shadow-lg); box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.06);
border: 1rpx solid rgba(0, 0, 0, 0.04);
overflow: hidden; overflow: hidden;
animation: fadeInScale 0.5s cubic-bezier(0.4, 0, 0.2, 1); animation: fadeInScale 0.5s cubic-bezier(0.4, 0, 0.2, 1);
} }
@ -47,8 +42,9 @@
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
height: 4rpx; height: 3rpx;
background: var(--primary-gradient); background: var(--primary-gradient);
border-radius: 20rpx 20rpx 0 0;
} }
.points-info { .points-info {

View File

@ -16,18 +16,12 @@
left: 0; left: 0;
right: 0; right: 0;
height: 280rpx; height: 280rpx;
background: linear-gradient(180deg, #FFF5F0 0%, var(--bg-page) 100%); background: transparent;
pointer-events: none; pointer-events: none;
} }
.hero-pattern { .hero-pattern {
position: absolute; display: none;
top: -60rpx;
right: -40rpx;
width: 220rpx;
height: 220rpx;
background: radial-gradient(circle, rgba(255, 107, 53, 0.08) 0%, transparent 70%);
border-radius: 50%;
} }
/* 页面标题 */ /* 页面标题 */
@ -35,16 +29,18 @@
position: relative; position: relative;
z-index: 1; z-index: 1;
text-align: center; text-align: center;
padding: 32rpx 24rpx 20rpx; padding: 48rpx 24rpx 32rpx;
animation: fadeInUp 0.5s cubic-bezier(0.4, 0, 0.2, 1); animation: fadeInUp 0.5s cubic-bezier(0.4, 0, 0.2, 1);
border-bottom: 1rpx solid rgba(0, 0, 0, 0.04);
} }
.page-title { .page-title {
display: block; display: block;
font-size: 36rpx; font-size: 52rpx;
font-weight: 700; font-weight: 700;
color: var(--text-primary); color: #1a1a1a;
letter-spacing: 2rpx; letter-spacing: 1rpx;
margin-bottom: 8rpx;
} }
/* 状态筛选标签 */ /* 状态筛选标签 */

View File

@ -15,18 +15,12 @@
left: 0; left: 0;
right: 0; right: 0;
height: 280rpx; height: 280rpx;
background: linear-gradient(180deg, #FFF5F0 0%, var(--bg-page) 100%); background: transparent;
pointer-events: none; pointer-events: none;
} }
.hero-pattern { .hero-pattern {
position: absolute; display: none;
top: -60rpx;
right: -40rpx;
width: 220rpx;
height: 220rpx;
background: radial-gradient(circle, rgba(255, 107, 53, 0.08) 0%, transparent 70%);
border-radius: 50%;
} }
/* 页面标题 */ /* 页面标题 */
@ -34,23 +28,26 @@
position: relative; position: relative;
z-index: 1; z-index: 1;
text-align: center; text-align: center;
padding: 32rpx 24rpx 24rpx; padding: 48rpx 24rpx 32rpx;
animation: fadeInUp 0.5s cubic-bezier(0.4, 0, 0.2, 1); animation: fadeInUp 0.5s cubic-bezier(0.4, 0, 0.2, 1);
border-bottom: 1rpx solid rgba(0, 0, 0, 0.04);
} }
.page-title { .page-title {
display: block; display: block;
font-size: 40rpx; font-size: 52rpx;
font-weight: 700; font-weight: 700;
color: var(--text-primary); color: #1a1a1a;
margin-bottom: 8rpx; margin-bottom: 8rpx;
letter-spacing: 2rpx; letter-spacing: 1rpx;
} }
.page-subtitle { .page-subtitle {
display: block; display: block;
font-size: 26rpx; font-size: 26rpx;
color: var(--text-muted); color: #999;
font-weight: 400;
letter-spacing: 0.5rpx;
} }
/* 积分概览卡片 */ /* 积分概览卡片 */

View File

@ -15,18 +15,12 @@
left: 0; left: 0;
right: 0; right: 0;
height: 320rpx; height: 320rpx;
background: linear-gradient(180deg, #FFF5F0 0%, var(--bg-page) 100%); background: transparent;
pointer-events: none; pointer-events: none;
} }
.hero-pattern { .hero-pattern {
position: absolute; display: none;
top: -80rpx;
right: -60rpx;
width: 280rpx;
height: 280rpx;
background: radial-gradient(circle, rgba(255, 107, 53, 0.1) 0%, transparent 70%);
border-radius: 50%;
} }
/* 页面标题 */ /* 页面标题 */
@ -34,23 +28,26 @@
position: relative; position: relative;
z-index: 1; z-index: 1;
text-align: center; text-align: center;
padding: 32rpx 24rpx 24rpx; padding: 48rpx 24rpx 32rpx;
animation: fadeInUp 0.5s cubic-bezier(0.4, 0, 0.2, 1); animation: fadeInUp 0.5s cubic-bezier(0.4, 0, 0.2, 1);
border-bottom: 1rpx solid rgba(0, 0, 0, 0.04);
} }
.page-title { .page-title {
display: block; display: block;
font-size: 40rpx; font-size: 52rpx;
font-weight: 700; font-weight: 700;
color: var(--text-primary); color: #1a1a1a;
margin-bottom: 8rpx; margin-bottom: 8rpx;
letter-spacing: 2rpx; letter-spacing: 1rpx;
} }
.page-subtitle { .page-subtitle {
display: block; display: block;
font-size: 26rpx; font-size: 26rpx;
color: var(--text-muted); color: #999;
font-weight: 400;
letter-spacing: 0.5rpx;
} }
/* 当前门店卡片 */ /* 当前门店卡片 */

View File

@ -64,6 +64,16 @@ Page({
try { try {
await app.getUserInfo() await app.getUserInfo()
// 如果当前门店有 ladderUserId确保获取该门店的天梯用户信息
if (app.globalData.currentStore?.storeId && !app.globalData.ladderUser) {
try {
await app.getLadderUser(app.globalData.currentStore.storeId)
} catch (e) {
console.error('获取天梯用户信息失败:', e)
}
}
this.setData({ this.setData({
userInfo: app.globalData.userInfo, userInfo: app.globalData.userInfo,
ladderUser: app.globalData.ladderUser, ladderUser: app.globalData.ladderUser,

View File

@ -115,11 +115,11 @@
<text class="record-label">胜场</text> <text class="record-label">胜场</text>
</view> </view>
<view class="record-item"> <view class="record-item">
<text class="record-value">{{ladderUser.matchCount - ladderUser.winCount || 0}}</text> <text class="record-value">{{(ladderUser.matchCount || 0) - (ladderUser.winCount || 0)}}</text>
<text class="record-label">负场</text> <text class="record-label">负场</text>
</view> </view>
<view class="record-item"> <view class="record-item">
<text class="record-value rate">{{ladderUser.matchCount > 0 ? Math.round(ladderUser.winCount / ladderUser.matchCount * 100) : 0}}%</text> <text class="record-value rate">{{(ladderUser.matchCount > 0 && ladderUser.winCount !== null && ladderUser.winCount !== undefined) ? Math.round((Number(ladderUser.winCount) || 0) / Number(ladderUser.matchCount) * 100) : 0}}%</text>
<text class="record-label">胜率</text> <text class="record-label">胜率</text>
</view> </view>
</view> </view>

View File

@ -17,26 +17,19 @@
left: 0; left: 0;
right: 0; right: 0;
height: 480rpx; height: 480rpx;
background: linear-gradient(180deg, #FFF5F0 0%, var(--bg-page) 100%); background: transparent;
pointer-events: none; pointer-events: none;
} }
.hero-pattern { .hero-pattern {
position: absolute; display: none;
top: -100rpx;
right: -80rpx;
width: 400rpx;
height: 400rpx;
background: radial-gradient(circle, rgba(255, 107, 53, 0.08) 0%, transparent 70%);
border-radius: 50%;
animation: pulse 4s ease-in-out infinite;
} }
/* 用户信息区域 */ /* 用户信息区域 */
.user-section { .user-section {
position: relative; position: relative;
z-index: 1; z-index: 1;
padding: 32rpx 24rpx 24rpx; padding: 48rpx 24rpx 24rpx;
} }
/* ========================================== /* ==========================================
@ -46,11 +39,12 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
padding: 48rpx 32rpx 40rpx; padding: 52rpx 32rpx 44rpx;
background: var(--bg-white); background: var(--bg-white);
border-radius: var(--radius-xl); border-radius: 24rpx;
box-shadow: var(--shadow-lg); box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.06);
margin-bottom: 20rpx; margin-bottom: 24rpx;
border: 1rpx solid rgba(0, 0, 0, 0.04);
animation: fadeInScale 0.5s cubic-bezier(0.4, 0, 0.2, 1); animation: fadeInScale 0.5s cubic-bezier(0.4, 0, 0.2, 1);
} }

View File

@ -16,6 +16,17 @@ NODE_ENV=development
# 服务端口默认3000 # 服务端口默认3000
PORT=3000 PORT=3000
# 基础URL用于生成完整的文件URL支持HTTPS
# 示例https://api.example.com 或 http://localhost:3000
# 如果设置了此项上传接口返回的URL将使用此值
# 如果不设置,将根据请求自动检测协议(支持反向代理)
BASE_URL=
# 强制使用HTTPStrue/false
# 如果设置为 true所有返回的URL将使用 https://
# 适用于部署在反向代理如nginx后面的情况
FORCE_HTTPS=false
# ------------------------------------------ # ------------------------------------------
# 数据库配置 (MySQL) # 数据库配置 (MySQL)
# ------------------------------------------ # ------------------------------------------

View File

@ -6,7 +6,8 @@
"scripts": { "scripts": {
"start": "node src/app.js", "start": "node src/app.js",
"dev": "nodemon src/app.js", "dev": "nodemon src/app.js",
"db:init": "node src/scripts/initDatabase.js" "db:init": "node src/scripts/initDatabase.js",
"db:mock": "node src/scripts/generateMockLadderData.js"
}, },
"dependencies": { "dependencies": {
"express": "^4.18.2", "express": "^4.18.2",

View File

@ -18,6 +18,10 @@ const uploadRoutes = require('./routes/upload');
const app = express(); const app = express();
const server = http.createServer(app); const server = http.createServer(app);
// 信任代理(支持反向代理的 HTTPS
// 如果应用部署在 nginx 等反向代理后面,需要设置此项
app.set('trust proxy', true);
// 中间件 // 中间件
app.use(cors()); app.use(cors());
app.use(express.json()); app.use(express.json());

View File

@ -142,6 +142,45 @@ class AdminController {
} }
} }
// 搜索用户(用于积分操作,不需要超级管理员权限)
async searchUsers(req, res) {
try {
const { keyword, pageSize = 10 } = req.query;
if (!keyword || keyword.trim().length < 2) {
return res.json(success([]));
}
const where = {
status: 1 // 只搜索正常状态的用户
};
where[Op.or] = [
{ nickname: { [Op.like]: `%${keyword.trim()}%` } },
{ phone: { [Op.like]: `%${keyword.trim()}%` } },
{ member_code: { [Op.like]: `%${keyword.trim()}%` } }
];
const users = await User.findAll({
where,
limit: parseInt(pageSize) || 10,
order: [['created_at', 'DESC']],
attributes: ['id', 'nickname', 'phone', 'avatar', 'member_code']
});
res.json(success(users.map(user => ({
id: user.id,
nickname: user.nickname,
phone: user.phone,
avatar: user.avatar,
memberCode: user.member_code
}))));
} catch (err) {
console.error('搜索用户失败:', err);
res.status(500).json(error('搜索失败'));
}
}
async getUserDetail(req, res) { async getUserDetail(req, res) {
try { try {
const { id } = req.params; const { id } = req.params;

View File

@ -114,6 +114,42 @@ class MatchController {
return res.status(400).json(error('天梯用户信息无效', 400)); return res.status(400).json(error('天梯用户信息无效', 400));
} }
// 检查发起者是否有未结束的挑战赛
const myOngoingMatch = await Match.findOne({
where: {
store_id,
type: MATCH_TYPES.CHALLENGE,
status: { [Op.in]: [MATCH_STATUS.PENDING, MATCH_STATUS.ONGOING] },
[Op.or]: [
{ challenger_id: myLadderUser.id },
{ defender_id: myLadderUser.id }
]
}
});
if (myOngoingMatch) {
await t.rollback();
return res.status(400).json(error('您有未结束的挑战赛,请先完成后再发起新的挑战', 400));
}
// 检查被挑战者是否有未结束的挑战赛
const targetOngoingMatch = await Match.findOne({
where: {
store_id,
type: MATCH_TYPES.CHALLENGE,
status: { [Op.in]: [MATCH_STATUS.PENDING, MATCH_STATUS.ONGOING] },
[Op.or]: [
{ challenger_id: targetLadderUser.id },
{ defender_id: targetLadderUser.id }
]
}
});
if (targetOngoingMatch) {
await t.rollback();
return res.status(400).json(error('对方有未结束的挑战赛,请稍后再试', 400));
}
// 创建挑战赛 // 创建挑战赛
const match = await Match.create({ const match = await Match.create({
store_id, store_id,
@ -184,6 +220,27 @@ class MatchController {
return res.status(403).json(error('您不是被挑战者', 403)); return res.status(403).json(error('您不是被挑战者', 403));
} }
// 如果接受挑战,检查是否有其他未结束的挑战赛
if (accept) {
const otherOngoingMatch = await Match.findOne({
where: {
store_id: match.store_id,
type: MATCH_TYPES.CHALLENGE,
status: { [Op.in]: [MATCH_STATUS.PENDING, MATCH_STATUS.ONGOING] },
id: { [Op.ne]: match_id },
[Op.or]: [
{ challenger_id: defenderLadderUser.id },
{ defender_id: defenderLadderUser.id }
]
}
});
if (otherOngoingMatch) {
await t.rollback();
return res.status(400).json(error('您有未结束的挑战赛,请先完成后再接受新的挑战', 400));
}
}
if (accept) { if (accept) {
// 接受挑战 // 接受挑战
await match.update({ status: MATCH_STATUS.ONGOING, start_time: new Date() }, { transaction: t }); await match.update({ status: MATCH_STATUS.ONGOING, start_time: new Date() }, { transaction: t });
@ -901,6 +958,7 @@ class MatchController {
async getMatchDetail(req, res) { async getMatchDetail(req, res) {
try { try {
const { id } = req.params; const { id } = req.params;
const user = req.user;
const match = await Match.findByPk(id, { const match = await Match.findByPk(id, {
include: [ include: [
@ -913,7 +971,179 @@ class MatchController {
return res.status(404).json(error('比赛不存在', 404)); return res.status(404).json(error('比赛不存在', 404));
} }
res.json(success({ // 获取当前用户的天梯用户信息
const myLadderUser = await LadderUser.findOne({
where: { user_id: user.id, store_id: match.store_id, status: 1 },
include: [{ model: User, as: 'user', attributes: ['nickname', 'avatar'] }]
});
// 调试日志
console.log('获取比赛详情 - 用户信息:', {
userId: user.id,
storeId: match.store_id,
myLadderUserId: myLadderUser?.id,
matchChallengerId: match.challenger_id,
matchDefenderId: match.defender_id,
matchStatus: match.status
});
let challengerInfo = null;
let defenderInfo = null;
let myRole = null; // 'challenger' | 'defender' | null
let canAccept = false;
let canReject = false;
let canSubmitScore = false;
let canConfirmScore = false;
if (match.type === MATCH_TYPES.CHALLENGE) {
// 挑战赛:获取挑战者和被挑战者信息
const challengerLadder = await LadderUser.findByPk(match.challenger_id, {
include: [{ model: User, as: 'user', attributes: ['nickname', 'avatar'] }]
});
const defenderLadder = await LadderUser.findByPk(match.defender_id, {
include: [{ model: User, as: 'user', attributes: ['nickname', 'avatar'] }]
});
if (challengerLadder) {
challengerInfo = {
id: challengerLadder.id,
realName: challengerLadder.real_name,
nickname: challengerLadder.user?.nickname,
avatar: challengerLadder.user?.avatar,
level: challengerLadder.level,
powerScore: challengerLadder.power_score,
userId: challengerLadder.user_id, // 添加 user_id用于前端判断
phone: challengerLadder.phone // 添加手机号,用于前端判断
};
}
if (defenderLadder) {
defenderInfo = {
id: defenderLadder.id,
realName: defenderLadder.real_name,
nickname: defenderLadder.user?.nickname,
avatar: defenderLadder.user?.avatar,
level: defenderLadder.level,
powerScore: defenderLadder.power_score,
userId: defenderLadder.user_id, // 添加 user_id用于前端判断
phone: defenderLadder.phone // 添加手机号,用于前端判断
};
}
// 确定当前用户角色
// 方法1通过 myLadderUser.id 比较(如果用户是该门店的天梯用户)
if (myLadderUser) {
// 使用 == 比较,因为可能是数字或字符串
if (myLadderUser.id == match.challenger_id) {
myRole = 'challenger';
console.log('用户是挑战者通过myLadderUser:', { myLadderUserId: myLadderUser.id, challengerId: match.challenger_id });
} else if (myLadderUser.id == match.defender_id) {
myRole = 'defender';
console.log('用户是被挑战者通过myLadderUser:', { myLadderUserId: myLadderUser.id, defenderId: match.defender_id });
} else {
console.log('用户角色不匹配通过myLadderUser:', {
myLadderUserId: myLadderUser.id,
challengerId: match.challenger_id,
defenderId: match.defender_id
});
}
}
// 方法2如果 myLadderUser 为 null通过挑战者和被挑战者的 user_id 来判断
if (!myRole && challengerLadder && defenderLadder) {
// 检查挑战者的 user_id
if (challengerLadder.user_id && challengerLadder.user_id == user.id) {
myRole = 'challenger';
console.log('用户是挑战者通过user_id:', {
userId: user.id,
challengerUserId: challengerLadder.user_id,
challengerLadderId: challengerLadder.id,
matchChallengerId: match.challenger_id
});
}
// 检查被挑战者的 user_id
else if (defenderLadder.user_id && defenderLadder.user_id == user.id) {
myRole = 'defender';
console.log('用户是被挑战者通过user_id:', {
userId: user.id,
defenderUserId: defenderLadder.user_id,
defenderLadderId: defenderLadder.id,
matchDefenderId: match.defender_id
});
} else {
console.log('用户角色不匹配通过user_id:', {
userId: user.id,
challengerUserId: challengerLadder.user_id,
challengerLadderId: challengerLadder.id,
defenderUserId: defenderLadder.user_id,
defenderLadderId: defenderLadder.id,
matchChallengerId: match.challenger_id,
matchDefenderId: match.defender_id
});
}
}
// 方法3如果前两种方法都失败尝试通过手机号匹配如果天梯用户有手机号且用户有手机号
if (!myRole && challengerLadder && defenderLadder && user.phone) {
if (challengerLadder.phone && challengerLadder.phone === user.phone) {
myRole = 'challenger';
console.log('用户是挑战者(通过手机号):', { userPhone: user.phone, challengerPhone: challengerLadder.phone });
} else if (defenderLadder.phone && defenderLadder.phone === user.phone) {
myRole = 'defender';
console.log('用户是被挑战者(通过手机号):', { userPhone: user.phone, defenderPhone: defenderLadder.phone });
}
}
if (!myRole) {
console.log('无法确定用户角色:', {
userId: user.id,
userPhone: user.phone,
storeId: match.store_id,
hasMyLadderUser: !!myLadderUser,
myLadderUserId: myLadderUser?.id,
challengerLadderId: challengerLadder?.id,
challengerUserId: challengerLadder?.user_id,
challengerPhone: challengerLadder?.phone,
defenderLadderId: defenderLadder?.id,
defenderUserId: defenderLadder?.user_id,
defenderPhone: defenderLadder?.phone,
matchChallengerId: match.challenger_id,
matchDefenderId: match.defender_id
});
}
// 判断操作权限
// 待接受状态且是被挑战者,可以接受或拒绝
if (match.status === MATCH_STATUS.PENDING) {
if (myRole === 'defender') {
canAccept = true;
canReject = true;
console.log('设置接受/拒绝权限为true:', { myRole, status: match.status, canAccept, canReject });
} else {
console.log('不能接受/拒绝:', { myRole, status: match.status, isDefender: myRole === 'defender' });
}
} else {
console.log('比赛状态不是待接受:', { status: match.status, myRole });
}
if (match.status === MATCH_STATUS.ONGOING && myLadderUser) {
const game = match.games?.[0];
if (game) {
// 检查是否可以提交比分(必须是胜者且未提交)
if (game.status === 1 && !game.submit_by) {
// 比赛进行中,还未提交比分,双方都可以填写
canSubmitScore = true;
} else if (game.status === 2 && game.submit_by && game.submit_by !== myLadderUser.id) {
// 对方已提交比分,等待我确认
if (game.confirm_status === CONFIRM_STATUS.PENDING) {
canConfirmScore = true;
}
}
}
}
}
const result = {
id: match.id, id: match.id,
matchCode: match.match_code, matchCode: match.match_code,
type: match.type, type: match.type,
@ -924,8 +1154,49 @@ class MatchController {
storeName: match.store?.name, storeName: match.store?.name,
startTime: match.start_time, startTime: match.start_time,
endTime: match.end_time, endTime: match.end_time,
games: match.games challenger: challengerInfo,
})); defender: defenderInfo,
myRole: myRole || null,
canAccept: Boolean(canAccept),
canReject: Boolean(canReject),
canSubmitScore: Boolean(canSubmitScore),
canConfirmScore: Boolean(canConfirmScore),
games: match.games?.map(game => ({
id: game.id,
player1Id: game.player1_id,
player2Id: game.player2_id,
player1Score: game.player1_score,
player2Score: game.player2_score,
winnerId: game.winner_id,
loserId: game.loser_id,
submitBy: game.submit_by,
confirmStatus: game.confirm_status,
status: game.status
})) || []
};
// 调试日志
console.log('比赛详情返回数据:', {
matchId: match.id,
matchStatus: match.status,
storeId: match.store_id,
userId: user.id,
myLadderUserId: myLadderUser?.id,
myLadderUserType: typeof myLadderUser?.id,
challengerId: match.challenger_id,
challengerIdType: typeof match.challenger_id,
defenderId: match.defender_id,
defenderIdType: typeof match.defender_id,
myRole,
canAccept: result.canAccept,
canReject: result.canReject,
hasMyLadderUser: !!myLadderUser,
challengerMatch: myLadderUser ? (myLadderUser.id == match.challenger_id) : false,
defenderMatch: myLadderUser ? (myLadderUser.id == match.defender_id) : false
});
// 确保返回所有必要的字段
res.json(success(result));
} catch (err) { } catch (err) {
console.error('获取比赛详情失败:', err); console.error('获取比赛详情失败:', err);
res.status(500).json(error('获取失败')); res.status(500).json(error('获取失败'));

View File

@ -128,8 +128,28 @@ class PointsAdminController {
return res.status(404).json(error('行为不存在', 404)); return res.status(404).json(error('行为不存在', 404));
} }
// 通过会员码获取用户 // 通过会员码、手机号或昵称获取用户
const user = await User.findOne({ where: { member_code } }); let user = null;
if (member_code) {
// 先尝试通过会员码查询
user = await User.findOne({ where: { member_code } });
// 如果会员码查询失败,尝试通过手机号查询
if (!user && /^1[3-9]\d{9}$/.test(member_code)) {
user = await User.findOne({ where: { phone: member_code } });
}
image.png
// 如果还是没找到,尝试通过昵称查询
if (!user) {
user = await User.findOne({
where: {
nickname: { [Op.like]: `%${member_code}%` },
status: 1
}
});
}
}
if (!user) { if (!user) {
await t.rollback(); await t.rollback();
return res.status(404).json(error('用户不存在', 404)); return res.status(404).json(error('用户不存在', 404));

View File

@ -14,6 +14,7 @@ router.get('/dashboard', authAdmin, adminController.getDashboard);
// === 用户管理(超管) === // === 用户管理(超管) ===
router.get('/users', authAdmin, requireSuperAdmin, adminController.getUsers); router.get('/users', authAdmin, requireSuperAdmin, adminController.getUsers);
router.get('/users/search', authAdmin, adminController.searchUsers); // 搜索用户(用于积分操作,普通管理员可用)
router.get('/users/:id', authAdmin, requireSuperAdmin, adminController.getUserDetail); router.get('/users/:id', authAdmin, requireSuperAdmin, adminController.getUserDetail);
router.put('/users/:id/status', authAdmin, requireSuperAdmin, adminController.updateUserStatus); router.put('/users/:id/status', authAdmin, requireSuperAdmin, adminController.updateUserStatus);

View File

@ -41,8 +41,9 @@ router.post('/image', authAdmin, upload.single('file'), (req, res) => {
return res.status(400).json(error('请选择要上传的图片')); return res.status(400).json(error('请选择要上传的图片'));
} }
const url = `/uploads/${req.file.filename}`; const relativePath = `/uploads/${req.file.filename}`;
res.json(success({ url }, '上传成功')); const fullUrl = getFullUrl(relativePath, req);
res.json(success({ url: fullUrl }, '上传成功'));
}); });
// 上传多张图片 // 上传多张图片
@ -51,7 +52,10 @@ router.post('/images', authAdmin, upload.array('files', 10), (req, res) => {
return res.status(400).json(error('请选择要上传的图片')); return res.status(400).json(error('请选择要上传的图片'));
} }
const urls = req.files.map(file => `/uploads/${file.filename}`); const urls = req.files.map(file => {
const relativePath = `/uploads/${file.filename}`;
return getFullUrl(relativePath, req);
});
res.json(success({ urls }, '上传成功')); res.json(success({ urls }, '上传成功'));
}); });

View File

@ -0,0 +1,168 @@
const sequelize = require('../config/database');
const { LadderUser, Store, User } = require('../models');
const { LADDER_LEVELS, POWER_CALC } = require('../config/constants');
// 中文姓名库
const surnames = ['张', '王', '李', '赵', '刘', '陈', '杨', '黄', '周', '吴', '徐', '孙', '马', '朱', '胡', '林', '郭', '何', '高', '罗'];
const givenNames = ['伟', '芳', '娜', '秀英', '敏', '静', '丽', '强', '磊', '军', '洋', '勇', '艳', '杰', '涛', '明', '超', '秀兰', '霞', '平', '刚', '桂英', '建华', '文', '华', '建国', '建军', '志强', '秀华', '秀珍'];
// 生成随机姓名
function generateName() {
const surname = surnames[Math.floor(Math.random() * surnames.length)];
const givenName = givenNames[Math.floor(Math.random() * givenNames.length)];
return surname + givenName;
}
// 生成随机手机号
function generatePhone() {
const prefixes = ['138', '139', '150', '151', '152', '158', '159', '186', '187', '188'];
const prefix = prefixes[Math.floor(Math.random() * prefixes.length)];
const suffix = Math.floor(10000000 + Math.random() * 90000000);
return prefix + suffix;
}
// 根据等级生成合理的战力值
function generatePowerScore(level) {
const baseScores = {
1: { min: 800, max: 1200 }, // 新锐
2: { min: 1200, max: 1600 }, // 精锐
3: { min: 1600, max: 2000 }, // 高手
4: { min: 2000, max: 2500 }, // 大师
5: { min: 2500, max: 3500 } // 宗师
};
const range = baseScores[level] || baseScores[1];
return Math.floor(range.min + Math.random() * (range.max - range.min));
}
// 生成比赛数据
function generateMatchData() {
const matchCount = Math.floor(3 + Math.random() * 50); // 3-53场
const monthlyMatchCount = Math.floor(3 + Math.random() * 15); // 3-18场确保>=3
const winCount = Math.floor(matchCount * (0.3 + Math.random() * 0.5)); // 胜率30%-80%
return { matchCount, monthlyMatchCount, winCount };
}
async function generateMockLadderData() {
try {
console.log('开始生成模拟天梯数据...\n');
// 获取第一个门店,如果没有则创建
let store = await Store.findOne({ where: { status: 1 } });
if (!store) {
console.log('未找到门店,创建默认门店...');
store = await Store.create({
name: '英飒羽毛球馆',
address: '测试地址',
contact: '13800138000',
status: 1
});
console.log(`已创建门店: ${store.name} (ID: ${store.id})\n`);
} else {
console.log(`使用门店: ${store.name} (ID: ${store.id})\n`);
}
const storeId = store.id;
const count = 50; // 生成50个天梯用户
const users = [];
console.log(`正在生成 ${count} 个天梯用户...`);
for (let i = 0; i < count; i++) {
const realName = generateName();
const phone = generatePhone();
const gender = Math.random() > 0.5 ? 1 : 2; // 随机性别
const level = Math.floor(1 + Math.random() * 5); // 1-5级
const powerScore = generatePowerScore(level);
const { matchCount, monthlyMatchCount, winCount } = generateMatchData();
// 检查手机号是否已存在
const existing = await LadderUser.findOne({
where: { store_id: storeId, phone }
});
if (existing) {
console.log(`跳过重复手机号: ${phone}`);
continue;
}
try {
const ladderUser = await LadderUser.create({
store_id: storeId,
real_name: realName,
phone: phone,
gender: gender,
level: level,
power_score: powerScore,
match_count: matchCount,
monthly_match_count: monthlyMatchCount,
win_count: winCount,
status: 1,
last_match_time: new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000) // 最近30天内
});
users.push({
id: ladderUser.id,
name: realName,
level,
powerScore,
matchCount,
winCount
});
if ((i + 1) % 10 === 0) {
console.log(`已生成 ${i + 1}/${count} 个用户...`);
}
} catch (err) {
console.error(`创建用户失败 (${realName}, ${phone}):`, err.message);
}
}
console.log(`\n✅ 成功生成 ${users.length} 个天梯用户!\n`);
// 显示统计信息
const stats = {
byLevel: {},
byGender: { 1: 0, 2: 0 },
totalPower: 0,
totalMatches: 0
};
const allUsers = await LadderUser.findAll({
where: { store_id: storeId, status: 1 },
order: [['power_score', 'DESC']]
});
allUsers.forEach(user => {
stats.byLevel[user.level] = (stats.byLevel[user.level] || 0) + 1;
stats.byGender[user.gender] = (stats.byGender[user.gender] || 0) + 1;
stats.totalPower += user.power_score;
stats.totalMatches += user.match_count;
});
console.log('📊 数据统计:');
console.log(` 总用户数: ${allUsers.length}`);
console.log(` 按等级分布:`);
console.log(` - 新锐 (Lv1): ${stats.byLevel[1] || 0}`);
console.log(` - 精锐 (Lv2): ${stats.byLevel[2] || 0}`);
console.log(` - 高手 (Lv3): ${stats.byLevel[3] || 0}`);
console.log(` - 大师 (Lv4): ${stats.byLevel[4] || 0}`);
console.log(` - 宗师 (Lv5): ${stats.byLevel[5] || 0}`);
console.log(` 按性别分布:`);
console.log(` - 男: ${stats.byGender[1]}`);
console.log(` - 女: ${stats.byGender[2]}`);
console.log(` 平均战力: ${Math.round(stats.totalPower / allUsers.length)}`);
console.log(` 总比赛场次: ${stats.totalMatches}`);
console.log(`\n🏆 排行榜前10名:`);
allUsers.slice(0, 10).forEach((user, index) => {
console.log(` ${index + 1}. ${user.real_name} - Lv${user.level} - 战力${user.power_score} - ${user.match_count}场 (${user.win_count}胜)`);
});
console.log('\n✨ 模拟数据生成完成!');
process.exit(0);
} catch (error) {
console.error('❌ 生成模拟数据失败:', error);
process.exit(1);
}
}
generateMockLadderData();

View File

@ -103,9 +103,45 @@ function getFullUrl(path, req) {
if (path.startsWith('http://') || path.startsWith('https://')) { if (path.startsWith('http://') || path.startsWith('https://')) {
return path; return path;
} }
// 相对路径,拼接服务器地址 // 相对路径,拼接服务器地址
const protocol = req?.protocol || 'http'; // 支持反向代理nginx等的 HTTPS 检测
const host = req?.get('host') || `localhost:${process.env.PORT || 3000}`; let protocol = 'http';
if (req) {
// 优先使用 X-Forwarded-Proto 头(反向代理设置)
const forwardedProto = req.get('X-Forwarded-Proto');
if (forwardedProto) {
protocol = forwardedProto;
} else {
// 使用 req.protocolExpress 会自动处理)
protocol = req.protocol;
}
// 如果设置了环境变量强制使用 HTTPS
if (process.env.FORCE_HTTPS === 'true') {
protocol = 'https';
}
}
// 获取主机地址
let host = `localhost:${process.env.PORT || 3000}`;
if (req) {
// 优先使用 X-Forwarded-Host反向代理设置
const forwardedHost = req.get('X-Forwarded-Host');
if (forwardedHost) {
host = forwardedHost;
} else {
host = req.get('host') || host;
}
}
// 如果设置了 BASE_URL 环境变量,直接使用
if (process.env.BASE_URL) {
const baseUrl = process.env.BASE_URL.replace(/\/$/, ''); // 移除末尾斜杠
const pathWithoutLeadingSlash = path.startsWith('/') ? path : `/${path}`;
return `${baseUrl}${pathWithoutLeadingSlash}`;
}
return `${protocol}://${host}${path}`; return `${protocol}://${host}${path}`;
} }