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:
parent
a3b4bd7cb0
commit
15413c85cc
197
admin/node_modules/.vite/deps/_metadata.json
generated
vendored
197
admin/node_modules/.vite/deps/_metadata.json
generated
vendored
@ -2,307 +2,370 @@
|
||||
"hash": "0b0fcdca",
|
||||
"configHash": "0bd4dba1",
|
||||
"lockfileHash": "45a4e0fd",
|
||||
"browserHash": "0f8eb059",
|
||||
"browserHash": "f9d297a9",
|
||||
"optimized": {
|
||||
"@element-plus/icons-vue": {
|
||||
"src": "../../@element-plus/icons-vue/dist/index.js",
|
||||
"file": "@element-plus_icons-vue.js",
|
||||
"fileHash": "63fef99e",
|
||||
"fileHash": "9f66cf3c",
|
||||
"needsInterop": false
|
||||
},
|
||||
"axios": {
|
||||
"src": "../../axios/index.js",
|
||||
"file": "axios.js",
|
||||
"fileHash": "c4c91103",
|
||||
"fileHash": "d0e8827a",
|
||||
"needsInterop": false
|
||||
},
|
||||
"dayjs": {
|
||||
"src": "../../dayjs/dayjs.min.js",
|
||||
"file": "dayjs.js",
|
||||
"fileHash": "45ab92bc",
|
||||
"fileHash": "67e70480",
|
||||
"needsInterop": true
|
||||
},
|
||||
"element-plus": {
|
||||
"src": "../../element-plus/es/index.mjs",
|
||||
"file": "element-plus.js",
|
||||
"fileHash": "a9eb7ac2",
|
||||
"fileHash": "6462e4a2",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/dist/locale/zh-cn.mjs": {
|
||||
"src": "../../element-plus/dist/locale/zh-cn.mjs",
|
||||
"file": "element-plus_dist_locale_zh-cn__mjs.js",
|
||||
"fileHash": "2e509bd7",
|
||||
"fileHash": "63d98427",
|
||||
"needsInterop": false
|
||||
},
|
||||
"pinia": {
|
||||
"src": "../../pinia/dist/pinia.mjs",
|
||||
"file": "pinia.js",
|
||||
"fileHash": "5573d424",
|
||||
"fileHash": "1cc5fe52",
|
||||
"needsInterop": false
|
||||
},
|
||||
"qrcode": {
|
||||
"src": "../../qrcode/lib/browser.js",
|
||||
"file": "qrcode.js",
|
||||
"fileHash": "2f402876",
|
||||
"fileHash": "8f92023e",
|
||||
"needsInterop": true
|
||||
},
|
||||
"vue": {
|
||||
"src": "../../vue/dist/vue.runtime.esm-bundler.js",
|
||||
"file": "vue.js",
|
||||
"fileHash": "0f609a55",
|
||||
"fileHash": "97abd90d",
|
||||
"needsInterop": false
|
||||
},
|
||||
"vue-router": {
|
||||
"src": "../../vue-router/dist/vue-router.mjs",
|
||||
"file": "vue-router.js",
|
||||
"fileHash": "25c74ef8",
|
||||
"fileHash": "b189be4f",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es": {
|
||||
"src": "../../element-plus/es/index.mjs",
|
||||
"file": "element-plus_es.js",
|
||||
"fileHash": "a13f022c",
|
||||
"fileHash": "4e5350bd",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/base/style/css": {
|
||||
"src": "../../element-plus/es/components/base/style/css.mjs",
|
||||
"file": "element-plus_es_components_base_style_css.js",
|
||||
"fileHash": "b4220b72",
|
||||
"fileHash": "2f24955e",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/loading/style/css": {
|
||||
"src": "../../element-plus/es/components/loading/style/css.mjs",
|
||||
"file": "element-plus_es_components_loading_style_css.js",
|
||||
"fileHash": "0122de81",
|
||||
"fileHash": "5a7f8660",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/dialog/style/css": {
|
||||
"src": "../../element-plus/es/components/dialog/style/css.mjs",
|
||||
"file": "element-plus_es_components_dialog_style_css.js",
|
||||
"fileHash": "f74604cc",
|
||||
"fileHash": "27c0aa21",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/input/style/css": {
|
||||
"src": "../../element-plus/es/components/input/style/css.mjs",
|
||||
"file": "element-plus_es_components_input_style_css.js",
|
||||
"fileHash": "664f43b9",
|
||||
"fileHash": "1e44aa38",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/pagination/style/css": {
|
||||
"src": "../../element-plus/es/components/pagination/style/css.mjs",
|
||||
"file": "element-plus_es_components_pagination_style_css.js",
|
||||
"fileHash": "ff29c02c",
|
||||
"fileHash": "c46c3f28",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/table/style/css": {
|
||||
"src": "../../element-plus/es/components/table/style/css.mjs",
|
||||
"file": "element-plus_es_components_table_style_css.js",
|
||||
"fileHash": "9516c0f3",
|
||||
"fileHash": "f2cad5d9",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/tag/style/css": {
|
||||
"src": "../../element-plus/es/components/tag/style/css.mjs",
|
||||
"file": "element-plus_es_components_tag_style_css.js",
|
||||
"fileHash": "acc11f71",
|
||||
"fileHash": "6026e653",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/table-column/style/css": {
|
||||
"src": "../../element-plus/es/components/table-column/style/css.mjs",
|
||||
"file": "element-plus_es_components_table-column_style_css.js",
|
||||
"fileHash": "63d6cae1",
|
||||
"fileHash": "20fe1fe1",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/form/style/css": {
|
||||
"src": "../../element-plus/es/components/form/style/css.mjs",
|
||||
"file": "element-plus_es_components_form_style_css.js",
|
||||
"fileHash": "8d500fe6",
|
||||
"fileHash": "acb1358d",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/form-item/style/css": {
|
||||
"src": "../../element-plus/es/components/form-item/style/css.mjs",
|
||||
"file": "element-plus_es_components_form-item_style_css.js",
|
||||
"fileHash": "dff655d7",
|
||||
"fileHash": "0d000125",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/select/style/css": {
|
||||
"src": "../../element-plus/es/components/select/style/css.mjs",
|
||||
"file": "element-plus_es_components_select_style_css.js",
|
||||
"fileHash": "18fa9e47",
|
||||
"fileHash": "f9c4f2b6",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/option/style/css": {
|
||||
"src": "../../element-plus/es/components/option/style/css.mjs",
|
||||
"file": "element-plus_es_components_option_style_css.js",
|
||||
"fileHash": "ec5bec4c",
|
||||
"fileHash": "5b17115f",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/button/style/css": {
|
||||
"src": "../../element-plus/es/components/button/style/css.mjs",
|
||||
"file": "element-plus_es_components_button_style_css.js",
|
||||
"fileHash": "4e0ee820",
|
||||
"fileHash": "072165a7",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/icon/style/css": {
|
||||
"src": "../../element-plus/es/components/icon/style/css.mjs",
|
||||
"file": "element-plus_es_components_icon_style_css.js",
|
||||
"fileHash": "d81f84e6",
|
||||
"fileHash": "91400fe8",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/container/style/css": {
|
||||
"src": "../../element-plus/es/components/container/style/css.mjs",
|
||||
"file": "element-plus_es_components_container_style_css.js",
|
||||
"fileHash": "05c2c583",
|
||||
"fileHash": "42960329",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/main/style/css": {
|
||||
"src": "../../element-plus/es/components/main/style/css.mjs",
|
||||
"file": "element-plus_es_components_main_style_css.js",
|
||||
"fileHash": "a80d717e",
|
||||
"fileHash": "79d917a7",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/header/style/css": {
|
||||
"src": "../../element-plus/es/components/header/style/css.mjs",
|
||||
"file": "element-plus_es_components_header_style_css.js",
|
||||
"fileHash": "e687ec69",
|
||||
"fileHash": "784162ca",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/dropdown/style/css": {
|
||||
"src": "../../element-plus/es/components/dropdown/style/css.mjs",
|
||||
"file": "element-plus_es_components_dropdown_style_css.js",
|
||||
"fileHash": "59835c76",
|
||||
"fileHash": "92871da6",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/dropdown-menu/style/css": {
|
||||
"src": "../../element-plus/es/components/dropdown-menu/style/css.mjs",
|
||||
"file": "element-plus_es_components_dropdown-menu_style_css.js",
|
||||
"fileHash": "23e3be80",
|
||||
"fileHash": "c0a9f980",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/dropdown-item/style/css": {
|
||||
"src": "../../element-plus/es/components/dropdown-item/style/css.mjs",
|
||||
"file": "element-plus_es_components_dropdown-item_style_css.js",
|
||||
"fileHash": "85b49429",
|
||||
"fileHash": "846c18b1",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/avatar/style/css": {
|
||||
"src": "../../element-plus/es/components/avatar/style/css.mjs",
|
||||
"file": "element-plus_es_components_avatar_style_css.js",
|
||||
"fileHash": "936aa490",
|
||||
"fileHash": "094b2abf",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/breadcrumb/style/css": {
|
||||
"src": "../../element-plus/es/components/breadcrumb/style/css.mjs",
|
||||
"file": "element-plus_es_components_breadcrumb_style_css.js",
|
||||
"fileHash": "875c584d",
|
||||
"fileHash": "6f37c7f8",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/breadcrumb-item/style/css": {
|
||||
"src": "../../element-plus/es/components/breadcrumb-item/style/css.mjs",
|
||||
"file": "element-plus_es_components_breadcrumb-item_style_css.js",
|
||||
"fileHash": "b43c6781",
|
||||
"fileHash": "8aabd46b",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/aside/style/css": {
|
||||
"src": "../../element-plus/es/components/aside/style/css.mjs",
|
||||
"file": "element-plus_es_components_aside_style_css.js",
|
||||
"fileHash": "e712c224",
|
||||
"fileHash": "3976d9c5",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/menu/style/css": {
|
||||
"src": "../../element-plus/es/components/menu/style/css.mjs",
|
||||
"file": "element-plus_es_components_menu_style_css.js",
|
||||
"fileHash": "b95e1cd9",
|
||||
"fileHash": "2fd5552a",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/menu-item/style/css": {
|
||||
"src": "../../element-plus/es/components/menu-item/style/css.mjs",
|
||||
"file": "element-plus_es_components_menu-item_style_css.js",
|
||||
"fileHash": "c8593318",
|
||||
"fileHash": "c1f41a14",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/input-number/style/css": {
|
||||
"src": "../../element-plus/es/components/input-number/style/css.mjs",
|
||||
"file": "element-plus_es_components_input-number_style_css.js",
|
||||
"fileHash": "67b6ba2c",
|
||||
"fileHash": "f2f57f0e",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/row/style/css": {
|
||||
"src": "../../element-plus/es/components/row/style/css.mjs",
|
||||
"file": "element-plus_es_components_row_style_css.js",
|
||||
"fileHash": "4e4350dd",
|
||||
"fileHash": "d116f174",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/col/style/css": {
|
||||
"src": "../../element-plus/es/components/col/style/css.mjs",
|
||||
"file": "element-plus_es_components_col_style_css.js",
|
||||
"fileHash": "43cd095d",
|
||||
"fileHash": "df159c48",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/radio-group/style/css": {
|
||||
"src": "../../element-plus/es/components/radio-group/style/css.mjs",
|
||||
"file": "element-plus_es_components_radio-group_style_css.js",
|
||||
"fileHash": "86c77dd9",
|
||||
"fileHash": "a88d5658",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/radio/style/css": {
|
||||
"src": "../../element-plus/es/components/radio/style/css.mjs",
|
||||
"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
|
||||
}
|
||||
},
|
||||
"chunks": {
|
||||
"chunk-4PW274X2": {
|
||||
"file": "chunk-4PW274X2.js"
|
||||
},
|
||||
"chunk-JUCAMQ7P": {
|
||||
"file": "chunk-JUCAMQ7P.js"
|
||||
},
|
||||
"chunk-SMFPDFTD": {
|
||||
"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": {
|
||||
"file": "chunk-R5DNQ3QC.js"
|
||||
},
|
||||
"chunk-B2YDYSZR": {
|
||||
"file": "chunk-B2YDYSZR.js"
|
||||
},
|
||||
"chunk-75C4BP7B": {
|
||||
"file": "chunk-75C4BP7B.js"
|
||||
},
|
||||
"chunk-5KK3TTMN": {
|
||||
"file": "chunk-5KK3TTMN.js"
|
||||
},
|
||||
"chunk-REWOA3VH": {
|
||||
"file": "chunk-REWOA3VH.js"
|
||||
},
|
||||
"chunk-TX5YLZ4O": {
|
||||
"file": "chunk-TX5YLZ4O.js"
|
||||
},
|
||||
"chunk-4PW274X2": {
|
||||
"file": "chunk-4PW274X2.js"
|
||||
},
|
||||
"chunk-IV6PSERC": {
|
||||
"file": "chunk-IV6PSERC.js"
|
||||
},
|
||||
"chunk-W7GFOP2W": {
|
||||
"file": "chunk-W7GFOP2W.js"
|
||||
},
|
||||
"chunk-OP4ZUAFM": {
|
||||
"file": "chunk-OP4ZUAFM.js"
|
||||
"chunk-UBLR4G7Q": {
|
||||
"file": "chunk-UBLR4G7Q.js"
|
||||
},
|
||||
"chunk-YFT6OQ5R": {
|
||||
"file": "chunk-YFT6OQ5R.js"
|
||||
},
|
||||
"chunk-NKQWFVTF": {
|
||||
"file": "chunk-NKQWFVTF.js"
|
||||
},
|
||||
"chunk-IV6PSERC": {
|
||||
"file": "chunk-IV6PSERC.js"
|
||||
},
|
||||
"chunk-STH2JMDO": {
|
||||
"file": "chunk-STH2JMDO.js"
|
||||
},
|
||||
"chunk-HYZ2CRGS": {
|
||||
"file": "chunk-HYZ2CRGS.js"
|
||||
},
|
||||
"chunk-H2732BJL": {
|
||||
"file": "chunk-H2732BJL.js"
|
||||
"chunk-OP4ZUAFM": {
|
||||
"file": "chunk-OP4ZUAFM.js"
|
||||
},
|
||||
"chunk-QZC7O2C6": {
|
||||
"file": "chunk-QZC7O2C6.js"
|
||||
},
|
||||
"chunk-H2732BJL": {
|
||||
"file": "chunk-H2732BJL.js"
|
||||
},
|
||||
"chunk-G3PMV62Z": {
|
||||
"file": "chunk-G3PMV62Z.js"
|
||||
}
|
||||
|
||||
73662
admin/node_modules/.vite/deps/chunk-W7GFOP2W.js
generated
vendored
73662
admin/node_modules/.vite/deps/chunk-W7GFOP2W.js
generated
vendored
File diff suppressed because it is too large
Load Diff
7
admin/node_modules/.vite/deps/chunk-W7GFOP2W.js.map
generated
vendored
7
admin/node_modules/.vite/deps/chunk-W7GFOP2W.js.map
generated
vendored
File diff suppressed because one or more lines are too long
6
admin/node_modules/.vite/deps/element-plus.js
generated
vendored
6
admin/node_modules/.vite/deps/element-plus.js
generated
vendored
@ -514,11 +514,11 @@ import {
|
||||
virtualizedScrollbarProps,
|
||||
watermarkProps,
|
||||
zIndexContextKey
|
||||
} from "./chunk-W7GFOP2W.js";
|
||||
import "./chunk-OP4ZUAFM.js";
|
||||
} from "./chunk-STH2JMDO.js";
|
||||
import "./chunk-HYZ2CRGS.js";
|
||||
import "./chunk-H2732BJL.js";
|
||||
import "./chunk-OP4ZUAFM.js";
|
||||
import "./chunk-QZC7O2C6.js";
|
||||
import "./chunk-H2732BJL.js";
|
||||
import "./chunk-G3PMV62Z.js";
|
||||
var export_dayjs = import_dayjs.default;
|
||||
export {
|
||||
|
||||
6
admin/node_modules/.vite/deps/element-plus_es.js
generated
vendored
6
admin/node_modules/.vite/deps/element-plus_es.js
generated
vendored
@ -514,11 +514,11 @@ import {
|
||||
virtualizedScrollbarProps,
|
||||
watermarkProps,
|
||||
zIndexContextKey
|
||||
} from "./chunk-W7GFOP2W.js";
|
||||
import "./chunk-OP4ZUAFM.js";
|
||||
} from "./chunk-STH2JMDO.js";
|
||||
import "./chunk-HYZ2CRGS.js";
|
||||
import "./chunk-H2732BJL.js";
|
||||
import "./chunk-OP4ZUAFM.js";
|
||||
import "./chunk-QZC7O2C6.js";
|
||||
import "./chunk-H2732BJL.js";
|
||||
import "./chunk-G3PMV62Z.js";
|
||||
var export_dayjs = import_dayjs.default;
|
||||
export {
|
||||
|
||||
4
admin/node_modules/.vite/deps/element-plus_es_components_pagination_style_css.js
generated
vendored
4
admin/node_modules/.vite/deps/element-plus_es_components_pagination_style_css.js
generated
vendored
@ -1,9 +1,9 @@
|
||||
import "./chunk-75C4BP7B.js";
|
||||
import "./chunk-5KK3TTMN.js";
|
||||
import "./chunk-UBLR4G7Q.js";
|
||||
import "./chunk-NKQWFVTF.js";
|
||||
import "./chunk-REWOA3VH.js";
|
||||
import "./chunk-TX5YLZ4O.js";
|
||||
import "./chunk-UBLR4G7Q.js";
|
||||
import "./chunk-NKQWFVTF.js";
|
||||
import "./chunk-IV6PSERC.js";
|
||||
|
||||
// node_modules/element-plus/es/components/pagination/style/css.mjs
|
||||
|
||||
2
admin/node_modules/.vite/deps/element-plus_es_components_select_style_css.js
generated
vendored
2
admin/node_modules/.vite/deps/element-plus_es_components_select_style_css.js
generated
vendored
@ -1,7 +1,7 @@
|
||||
import "./chunk-75C4BP7B.js";
|
||||
import "./chunk-5KK3TTMN.js";
|
||||
import "./chunk-UBLR4G7Q.js";
|
||||
import "./chunk-REWOA3VH.js";
|
||||
import "./chunk-TX5YLZ4O.js";
|
||||
import "./chunk-UBLR4G7Q.js";
|
||||
import "./chunk-IV6PSERC.js";
|
||||
//# sourceMappingURL=element-plus_es_components_select_style_css.js.map
|
||||
|
||||
2
admin/node_modules/.vite/deps/element-plus_es_components_table-column_style_css.js
generated
vendored
2
admin/node_modules/.vite/deps/element-plus_es_components_table-column_style_css.js
generated
vendored
@ -1,5 +1,5 @@
|
||||
import "./chunk-5KK3TTMN.js";
|
||||
import "./chunk-B2YDYSZR.js";
|
||||
import "./chunk-5KK3TTMN.js";
|
||||
import "./chunk-IV6PSERC.js";
|
||||
|
||||
// node_modules/element-plus/es/components/table-column/style/css.mjs
|
||||
|
||||
@ -17,6 +17,7 @@ export const getDashboard = () => request.get('/admin/dashboard')
|
||||
|
||||
// === 用户管理 ===
|
||||
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 updateUserStatus = (id, data) => request.put(`/admin/users/${id}/status`, data)
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -89,6 +89,29 @@ App({
|
||||
if (loginRes.data.code === 0) {
|
||||
this.globalData.token = loginRes.data.data.token;
|
||||
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);
|
||||
this.connectWebSocket();
|
||||
resolve(loginRes.data.data);
|
||||
@ -112,6 +135,29 @@ App({
|
||||
this.request("/api/user/info")
|
||||
.then((res) => {
|
||||
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();
|
||||
resolve(res.data);
|
||||
})
|
||||
@ -131,9 +177,25 @@ App({
|
||||
})
|
||||
.then((res) => {
|
||||
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);
|
||||
})
|
||||
.catch(reject);
|
||||
@ -143,6 +205,25 @@ App({
|
||||
this.request("/api/user/current-store")
|
||||
.then((res) => {
|
||||
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);
|
||||
})
|
||||
.catch(reject);
|
||||
@ -214,23 +295,64 @@ App({
|
||||
handleWsMessage(data) {
|
||||
switch (data.type) {
|
||||
case "challenge_request":
|
||||
// 收到挑战请求
|
||||
wx.showModal({
|
||||
title: "收到挑战",
|
||||
content: `${data.data.challenger.realName} 向你发起挑战`,
|
||||
confirmText: "接受",
|
||||
cancelText: "拒绝",
|
||||
success: (res) => {
|
||||
this.request(
|
||||
"/api/match/challenge/respond",
|
||||
{
|
||||
match_id: data.data.matchId,
|
||||
accept: res.confirm,
|
||||
},
|
||||
"POST"
|
||||
);
|
||||
},
|
||||
});
|
||||
// 收到挑战请求 - 使用自定义弹框
|
||||
const challengeData = data.data;
|
||||
const pages = getCurrentPages();
|
||||
const currentPage = pages[pages.length - 1];
|
||||
|
||||
// 如果当前页面有处理挑战请求的方法,调用它
|
||||
if (currentPage && typeof currentPage.handleChallengeRequest === 'function') {
|
||||
currentPage.handleChallengeRequest(challengeData);
|
||||
} else {
|
||||
// 否则使用系统弹框
|
||||
wx.showModal({
|
||||
title: "收到挑战",
|
||||
content: `${challengeData.challenger.realName}(Lv${challengeData.challenger.level}, 战力${challengeData.challenger.powerScore}) 向你发起挑战`,
|
||||
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;
|
||||
case "score_confirm_request":
|
||||
// 收到比分确认请求
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
"pages/index/index",
|
||||
"pages/user/index",
|
||||
"pages/match/challenge/index",
|
||||
"pages/match/challenge-detail/index",
|
||||
"pages/match/ranking/index",
|
||||
"pages/match/history/index",
|
||||
"pages/points/mall/index",
|
||||
|
||||
@ -376,10 +376,6 @@ page {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.power-change.positive::before {
|
||||
content: '+';
|
||||
}
|
||||
|
||||
.power-change.negative {
|
||||
color: #FF4D4F;
|
||||
}
|
||||
|
||||
@ -6,17 +6,17 @@
|
||||
// 开发环境配置
|
||||
const devConfig = {
|
||||
// API 基础地址(本地开发)
|
||||
baseUrl: "http://localhost:3000",
|
||||
baseUrl: "https://yingsa-server.ethan.team",
|
||||
// WebSocket 地址(本地开发)
|
||||
wsUrl: "ws://localhost:3000/ws",
|
||||
wsUrl: "wss://yingsa-server.ethan.team/ws",
|
||||
};
|
||||
|
||||
// 生产环境配置
|
||||
const prodConfig = {
|
||||
// API 基础地址(生产环境,请替换为实际域名)
|
||||
baseUrl: "https://your-domain.com",
|
||||
baseUrl: "https://yingsa-server.ethan.team",
|
||||
// WebSocket 地址(生产环境,请替换为实际域名)
|
||||
wsUrl: "wss://your-domain.com/ws",
|
||||
wsUrl: "wss://yingsa-server.ethan.team/ws",
|
||||
};
|
||||
|
||||
// 根据环境变量选择配置
|
||||
|
||||
@ -26,94 +26,85 @@
|
||||
|
||||
<!-- 主要内容区域 -->
|
||||
<view class="main-content">
|
||||
<!-- 性别筛选标签 -->
|
||||
<view class="filter-bar animate-fadeInUp" style="animation-delay: 0.1s">
|
||||
<view
|
||||
class="filter-item {{gender === '' ? 'active' : ''}}"
|
||||
bindtap="setGender"
|
||||
data-gender=""
|
||||
>
|
||||
全部
|
||||
</view>
|
||||
<view
|
||||
class="filter-item {{gender === '1' ? 'active' : ''}}"
|
||||
bindtap="setGender"
|
||||
data-gender="1"
|
||||
>
|
||||
♂ 男子
|
||||
</view>
|
||||
<view
|
||||
class="filter-item {{gender === '2' ? 'active' : ''}}"
|
||||
bindtap="setGender"
|
||||
data-gender="2"
|
||||
>
|
||||
♀ 女子
|
||||
<!-- 性别筛选标签 - 吸附在顶部 -->
|
||||
<view class="filter-bar-wrapper">
|
||||
<view class="filter-bar animate-fadeInUp" style="animation-delay: 0.1s">
|
||||
<view
|
||||
class="filter-item {{gender === '' ? 'active' : ''}}"
|
||||
bindtap="setGender"
|
||||
data-gender=""
|
||||
>
|
||||
全部
|
||||
</view>
|
||||
<view
|
||||
class="filter-item {{gender === '1' ? 'active' : ''}}"
|
||||
bindtap="setGender"
|
||||
data-gender="1"
|
||||
>
|
||||
♂ 男子
|
||||
</view>
|
||||
<view
|
||||
class="filter-item {{gender === '2' ? 'active' : ''}}"
|
||||
bindtap="setGender"
|
||||
data-gender="2"
|
||||
>
|
||||
♀ 女子
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 排名列表 -->
|
||||
<view class="ranking-list">
|
||||
<scroll-view
|
||||
scroll-y="true"
|
||||
class="ranking-scroll"
|
||||
bindscrolltolower="loadMore"
|
||||
lower-threshold="100"
|
||||
style="height: calc(100vh - 480rpx);"
|
||||
>
|
||||
<block wx:if="{{list.length > 0}}">
|
||||
<view
|
||||
class="ranking-item stagger-item {{index < 3 ? 'top-rank' : ''}} animate-fadeInUp"
|
||||
wx:for="{{list}}"
|
||||
wx:key="id"
|
||||
bindtap="viewPlayer"
|
||||
data-id="{{item.id}}"
|
||||
>
|
||||
<!-- 排名徽章 -->
|
||||
<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>
|
||||
<block wx:if="{{list.length > 0}}">
|
||||
<view
|
||||
class="ranking-item stagger-item {{index < 3 ? 'top-rank' : ''}} animate-fadeInUp"
|
||||
wx:for="{{list}}"
|
||||
wx:key="id"
|
||||
bindtap="viewPlayer"
|
||||
data-id="{{item.id}}"
|
||||
>
|
||||
<!-- 排名徽章 -->
|
||||
<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>
|
||||
|
||||
<!-- 选手头像 -->
|
||||
<image class="player-avatar" src="{{item.avatar || '/images/avatar-default.svg'}}" mode="aspectFill"></image>
|
||||
<!-- 选手头像 -->
|
||||
<image class="player-avatar" src="{{item.avatar || '/images/avatar-default.svg'}}" mode="aspectFill"></image>
|
||||
|
||||
<!-- 选手信息 -->
|
||||
<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 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>
|
||||
</block>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view wx:elif="{{!loading}}" class="empty-state">
|
||||
<image class="empty-icon" src="/images/empty-ranking.svg" mode="aspectFit"></image>
|
||||
<text class="empty-title">暂无排名数据</text>
|
||||
<text class="empty-desc">每月完成3场比赛即可上榜</text>
|
||||
<!-- 战力值 -->
|
||||
<view class="player-power">
|
||||
<text class="power-value">{{item.powerScore}}</text>
|
||||
<text class="power-label">战力</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view wx:if="{{loading}}" class="loading-state">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
<!-- 空状态 -->
|
||||
<view wx:elif="{{!loading}}" class="empty-state">
|
||||
<image class="empty-icon" src="/images/empty-ranking.svg" mode="aspectFit"></image>
|
||||
<text class="empty-title">暂无排名数据</text>
|
||||
<text class="empty-desc">每月完成3场比赛即可上榜</text>
|
||||
</view>
|
||||
|
||||
<!-- 到底提示 -->
|
||||
<view wx:if="{{list.length > 0 && !hasMore && !loading}}" class="load-more">
|
||||
<text>— 已显示全部选手 —</text>
|
||||
</view>
|
||||
<!-- 加载更多 -->
|
||||
<view wx:if="{{loading}}" class="loading-state">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 底部安全区域 -->
|
||||
<view class="safe-bottom"></view>
|
||||
</scroll-view>
|
||||
<!-- 到底提示 -->
|
||||
<view wx:if="{{list.length > 0 && !hasMore && !loading}}" class="load-more">
|
||||
<text>— 已显示全部选手 —</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@ -6,35 +6,22 @@
|
||||
min-height: 100vh;
|
||||
background: var(--bg-page);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 顶部装饰背景 */
|
||||
.hero-section {
|
||||
position: relative;
|
||||
padding: 32rpx 24rpx 24rpx;
|
||||
background: linear-gradient(180deg, #FFF5F0 0%, var(--bg-page) 100%);
|
||||
padding: 48rpx 24rpx 32rpx;
|
||||
background: transparent;
|
||||
border-bottom: 1rpx solid rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.hero-pattern {
|
||||
position: absolute;
|
||||
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;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hero-pattern-2 {
|
||||
position: absolute;
|
||||
top: 120rpx;
|
||||
left: -80rpx;
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
background: radial-gradient(circle, rgba(0, 201, 167, 0.06) 0%, transparent 70%);
|
||||
border-radius: 50%;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 门店信息头部 */
|
||||
@ -44,7 +31,11 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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);
|
||||
}
|
||||
|
||||
@ -58,24 +49,25 @@
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
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;
|
||||
}
|
||||
|
||||
.store-name {
|
||||
font-size: 28rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
color: #333;
|
||||
letter-spacing: 0.5rpx;
|
||||
}
|
||||
|
||||
.change-store-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
padding: 12rpx 20rpx;
|
||||
background: var(--bg-white);
|
||||
border-radius: var(--radius-full);
|
||||
box-shadow: var(--shadow-sm);
|
||||
padding: 10rpx 18rpx;
|
||||
background: var(--bg-soft);
|
||||
border-radius: 20rpx;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
@ -85,12 +77,14 @@
|
||||
|
||||
.change-store-text {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-secondary);
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.change-store-arrow {
|
||||
font-size: 20rpx;
|
||||
color: var(--text-muted);
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
/* 页面标题 */
|
||||
@ -98,34 +92,48 @@
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
margin-bottom: 8rpx;
|
||||
margin-bottom: 0;
|
||||
padding: 24rpx 0;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
display: block;
|
||||
font-size: 44rpx;
|
||||
font-size: 52rpx;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
color: #1a1a1a;
|
||||
margin-bottom: 8rpx;
|
||||
letter-spacing: 2rpx;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: var(--text-muted);
|
||||
color: #999;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.5rpx;
|
||||
}
|
||||
|
||||
/* 主要内容区域 */
|
||||
.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 {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
@ -141,9 +149,10 @@
|
||||
}
|
||||
|
||||
.filter-item.active {
|
||||
background: var(--primary-gradient);
|
||||
color: var(--text-white);
|
||||
box-shadow: var(--shadow-primary);
|
||||
background: linear-gradient(135deg, #ff6b35 0%, #ff8c42 100%);
|
||||
color: #fff;
|
||||
box-shadow: 0 4rpx 16rpx rgba(255, 107, 53, 0.3);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.filter-item:active {
|
||||
@ -152,82 +161,93 @@
|
||||
|
||||
/* 排名列表 */
|
||||
.ranking-list {
|
||||
|
||||
}
|
||||
|
||||
.ranking-scroll {
|
||||
|
||||
padding: 0 24rpx 40rpx;
|
||||
}
|
||||
|
||||
.ranking-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx 24rpx;
|
||||
background: var(--bg-white);
|
||||
border-radius: var(--radius-lg);
|
||||
margin-bottom: 12rpx;
|
||||
box-shadow: var(--shadow-sm);
|
||||
padding: 24rpx;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
margin-bottom: 16rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
|
||||
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 {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.ranking-item.top-rank {
|
||||
background: linear-gradient(135deg, #FFFBF8, var(--bg-white));
|
||||
border: 1rpx solid rgba(255, 107, 53, 0.1);
|
||||
background: linear-gradient(135deg, #fff5f0 0%, #fff 100%);
|
||||
border: 2rpx solid rgba(255, 107, 53, 0.2);
|
||||
box-shadow: 0 6rpx 20rpx rgba(255, 107, 53, 0.15);
|
||||
}
|
||||
|
||||
/* 排名徽章 */
|
||||
.rank-badge {
|
||||
width: 52rpx;
|
||||
height: 52rpx;
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
margin-right: 16rpx;
|
||||
margin-right: 20rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.rank-badge.top1 {
|
||||
background: linear-gradient(135deg, #FFE082 0%, #FFD700 100%);
|
||||
background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
|
||||
color: #8B4513;
|
||||
box-shadow: 0 4rpx 16rpx rgba(255, 215, 0, 0.4);
|
||||
font-size: 28rpx;
|
||||
box-shadow: 0 6rpx 20rpx rgba(255, 215, 0, 0.5);
|
||||
font-size: 32rpx;
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
}
|
||||
|
||||
.rank-badge.top2 {
|
||||
background: linear-gradient(135deg, #F5F5F5 0%, #C0C0C0 100%);
|
||||
background: linear-gradient(135deg, #E8E8E8 0%, #C0C0C0 100%);
|
||||
color: #4A4A4A;
|
||||
box-shadow: 0 4rpx 12rpx rgba(192, 192, 192, 0.4);
|
||||
font-size: 28rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(192, 192, 192, 0.5);
|
||||
font-size: 32rpx;
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
}
|
||||
|
||||
.rank-badge.top3 {
|
||||
background: linear-gradient(135deg, #DEB887 0%, #CD853F 100%);
|
||||
color: #5C4033;
|
||||
box-shadow: 0 4rpx 12rpx rgba(205, 133, 63, 0.4);
|
||||
font-size: 28rpx;
|
||||
background: linear-gradient(135deg, #CD853F 0%, #B8860B 100%);
|
||||
color: #fff;
|
||||
box-shadow: 0 4rpx 16rpx rgba(205, 133, 63, 0.5);
|
||||
font-size: 32rpx;
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
}
|
||||
|
||||
.rank-badge.normal {
|
||||
background: var(--bg-soft);
|
||||
color: var(--text-muted);
|
||||
background: linear-gradient(135deg, #f5f5f5 0%, #e8e8e8 100%);
|
||||
color: #666;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 选手头像 */
|
||||
.player-avatar {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 50%;
|
||||
margin-right: 16rpx;
|
||||
border: 2rpx solid var(--bg-white);
|
||||
box-shadow: var(--shadow-sm);
|
||||
margin-right: 20rpx;
|
||||
border: 3rpx solid #fff;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
flex-shrink: 0;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
/* 选手信息 */
|
||||
@ -238,10 +258,10 @@
|
||||
|
||||
.player-name {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 6rpx;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@ -250,15 +270,18 @@
|
||||
.player-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
gap: 16rpx;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.player-level {
|
||||
display: inline-flex;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: var(--radius-full);
|
||||
font-size: 20rpx;
|
||||
align-items: center;
|
||||
padding: 4rpx 14rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.player-level.lv1 { background: #E8F5E9; color: #2E7D32; }
|
||||
@ -268,8 +291,9 @@
|
||||
.player-level.lv5 { background: #F3E5F5; color: #7B1FA2; }
|
||||
|
||||
.player-stats {
|
||||
font-size: 22rpx;
|
||||
color: var(--text-muted);
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 战力值 */
|
||||
@ -280,14 +304,17 @@
|
||||
|
||||
.power-value {
|
||||
display: block;
|
||||
font-size: 32rpx;
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: var(--primary);
|
||||
color: #ff6b35;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.power-label {
|
||||
font-size: 22rpx;
|
||||
color: var(--text-muted);
|
||||
color: #999;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
@ -329,12 +356,8 @@
|
||||
/* 加载更多 */
|
||||
.load-more {
|
||||
text-align: center;
|
||||
padding: 32rpx;
|
||||
padding: 16rpx 32rpx 8rpx;
|
||||
color: var(--text-muted);
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
/* 底部安全区域 */
|
||||
.safe-bottom {
|
||||
height: 80rpx;
|
||||
}
|
||||
|
||||
588
miniprogram/pages/match/challenge-detail/index.js
Normal file
588
miniprogram/pages/match/challenge-detail/index.js
Normal 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 的 id(ladder_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 的 id(ladder_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() {
|
||||
// 空函数,用于阻止事件冒泡
|
||||
}
|
||||
})
|
||||
5
miniprogram/pages/match/challenge-detail/index.json
Normal file
5
miniprogram/pages/match/challenge-detail/index.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"navigationBarTitleText": "挑战赛详情",
|
||||
"enablePullDownRefresh": true,
|
||||
"backgroundColor": "#f5f7fa"
|
||||
}
|
||||
121
miniprogram/pages/match/challenge-detail/index.wxml
Normal file
121
miniprogram/pages/match/challenge-detail/index.wxml
Normal 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>
|
||||
383
miniprogram/pages/match/challenge-detail/index.wxss
Normal file
383
miniprogram/pages/match/challenge-detail/index.wxss
Normal 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);
|
||||
}
|
||||
@ -172,16 +172,27 @@ Page({
|
||||
wx.showLoading({ title: '发起挑战中...' })
|
||||
|
||||
try {
|
||||
await app.request('/api/match/challenge/create', {
|
||||
const res = await app.request('/api/match/challenge/create', {
|
||||
store_id: this.data.currentStore.storeId,
|
||||
target_member_code: memberCode
|
||||
}, 'POST')
|
||||
|
||||
wx.hideLoading()
|
||||
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) {
|
||||
wx.hideLoading()
|
||||
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) {
|
||||
const match = e.currentTarget.dataset.match
|
||||
if (match.type === 1) {
|
||||
// 挑战赛详情 - 暂时跳转到历史记录页
|
||||
// 挑战赛详情
|
||||
wx.navigateTo({
|
||||
url: `/pages/match/history/index`
|
||||
url: `/pages/match/challenge-detail/index?id=${match.id}`
|
||||
})
|
||||
} else {
|
||||
// 排位赛详情
|
||||
|
||||
@ -16,32 +16,17 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 420rpx;
|
||||
background: linear-gradient(135deg, #FF8A65 0%, #FF6B35 50%, #F4511E 100%);
|
||||
border-radius: 0 0 60rpx 60rpx;
|
||||
background: transparent;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hero-bg::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -100rpx;
|
||||
right: -80rpx;
|
||||
width: 300rpx;
|
||||
height: 300rpx;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 50%;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hero-bg::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -60rpx;
|
||||
left: -60rpx;
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 50%;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hero-pattern {
|
||||
@ -70,23 +55,25 @@
|
||||
/* 页面标题 */
|
||||
.page-header {
|
||||
text-align: center;
|
||||
padding: 20rpx 0 30rpx;
|
||||
padding: 32rpx 0 40rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
display: block;
|
||||
font-size: 48rpx;
|
||||
font-weight: 800;
|
||||
color: #fff;
|
||||
margin-bottom: 10rpx;
|
||||
text-shadow: 0 4rpx 10rpx rgba(0, 0, 0, 0.15);
|
||||
letter-spacing: 4rpx;
|
||||
font-size: 52rpx;
|
||||
font-weight: 700;
|
||||
color: #1a1a1a;
|
||||
margin-bottom: 12rpx;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
display: block;
|
||||
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;
|
||||
justify-content: center;
|
||||
gap: 10rpx;
|
||||
padding: 16rpx 24rpx;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 50rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(255, 107, 53, 0.2);
|
||||
padding: 20rpx 24rpx;
|
||||
background: var(--bg-white);
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
margin: 0 auto 24rpx;
|
||||
width: fit-content;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1rpx solid rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.store-icon {
|
||||
|
||||
@ -51,10 +51,31 @@ Page({
|
||||
pageSize: this.data.pageSize
|
||||
})
|
||||
|
||||
const matches = (res.data.list || []).map(match => ({
|
||||
...match,
|
||||
confirmedAt: util.formatDate(match.confirmedAt)
|
||||
}))
|
||||
const matches = (res.data.list || []).map(match => {
|
||||
// 确保 powerChange 是数字类型,移除可能存在的加号和其他非数字字符
|
||||
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({
|
||||
matches: this.data.page === 1 ? matches : [...this.data.matches, ...matches],
|
||||
|
||||
@ -7,15 +7,17 @@
|
||||
<text class="match-type">{{item.matchType === 1 ? '挑战赛' : '排位赛'}}</text>
|
||||
</view>
|
||||
<view class="match-content">
|
||||
<view class="result {{item.isWin ? 'win' : 'lose'}}">
|
||||
{{item.isWin ? '胜' : '负'}}
|
||||
</view>
|
||||
<view class="score-info">
|
||||
<text class="opponent">vs {{item.opponentName}}</text>
|
||||
<text class="score">{{item.myScore}} : {{item.opponentScore}}</text>
|
||||
</view>
|
||||
<view class="power-change {{item.powerChange > 0 ? 'positive' : 'negative'}}">
|
||||
{{item.powerChange > 0 ? '+' : ''}}{{item.powerChange}}
|
||||
<view class="content-row">
|
||||
<view class="result {{item.isWin ? 'win' : 'lose'}}">
|
||||
{{item.isWin ? '胜' : '负'}}
|
||||
</view>
|
||||
<view class="score-info">
|
||||
<text class="opponent">vs {{item.opponentName}}</text>
|
||||
<text class="score">{{item.myScore}} : {{item.opponentScore}}</text>
|
||||
</view>
|
||||
<view class="power-change {{item.powerChange > 0 ? 'positive' : 'negative'}}">
|
||||
<text wx:if="{{item.powerChange > 0}}">+</text><text>{{item.powerChange}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="match-footer">
|
||||
|
||||
@ -1,364 +1,166 @@
|
||||
/* ==========================================
|
||||
比赛记录页面 - 浅色高级感设计
|
||||
比赛记录页面 - 橙色主题优化
|
||||
========================================== */
|
||||
|
||||
.page-container {
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background: var(--bg-page);
|
||||
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;
|
||||
background: #f5f5f5;
|
||||
padding: 20rpx;
|
||||
background: var(--bg-white);
|
||||
border-radius: var(--radius-full);
|
||||
box-shadow: var(--shadow-sm);
|
||||
font-size: 26rpx;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.history-list {
|
||||
display: flex;
|
||||
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;
|
||||
}
|
||||
|
||||
.filter-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 {
|
||||
.match-item:active {
|
||||
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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20rpx 24rpx;
|
||||
background: linear-gradient(90deg, #FAFBFC, var(--bg-white));
|
||||
border-bottom: 1rpx solid var(--border-soft);
|
||||
align-items: center;
|
||||
padding: 24rpx 28rpx;
|
||||
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 {
|
||||
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;
|
||||
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;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.type-badge {
|
||||
padding: 6rpx 14rpx;
|
||||
border-radius: var(--radius-full);
|
||||
font-size: 22rpx;
|
||||
.opponent {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.score {
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
|
||||
.power-change {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 12rpx;
|
||||
text-align: center;
|
||||
min-width: 100rpx;
|
||||
}
|
||||
|
||||
.type-badge.ladder {
|
||||
background: var(--primary-soft);
|
||||
color: var(--primary);
|
||||
.power-change.positive {
|
||||
background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%);
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.type-badge.friendly {
|
||||
background: var(--accent-light);
|
||||
color: var(--accent);
|
||||
.power-change.negative {
|
||||
background: linear-gradient(135deg, #ffebee 0%, #ffcdd2 100%);
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
.result-badge {
|
||||
padding: 6rpx 14rpx;
|
||||
border-radius: var(--radius-full);
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.result-badge.win {
|
||||
background: var(--accent-light);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.result-badge.lose {
|
||||
background: #FFF1F0;
|
||||
color: #FF4D4F;
|
||||
.match-footer {
|
||||
padding: 20rpx 28rpx;
|
||||
background: #fafafa;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.match-time {
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 100rpx 48rpx;
|
||||
animation: fadeInUp 0.5s cubic-bezier(0.4, 0, 0.2, 1) 0.2s backwards;
|
||||
justify-content: center;
|
||||
padding: 120rpx 40rpx;
|
||||
min-height: 60vh;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
width: 180rpx;
|
||||
height: 180rpx;
|
||||
.empty-state image {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
opacity: 0.6;
|
||||
margin-bottom: 32rpx;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 12rpx;
|
||||
.empty-state text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
font-size: 26rpx;
|
||||
color: var(--text-muted);
|
||||
text-align: center;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48rpx;
|
||||
color: var(--text-muted);
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
/* 底部安全区域 */
|
||||
.safe-bottom {
|
||||
height: 80rpx;
|
||||
}
|
||||
|
||||
/* 加载更多 */
|
||||
.load-more {
|
||||
text-align: center;
|
||||
padding: 32rpx;
|
||||
color: var(--text-muted);
|
||||
font-size: 26rpx;
|
||||
padding: 40rpx;
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
@ -16,32 +16,17 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 380rpx;
|
||||
background: linear-gradient(135deg, #FFB74D 0%, #FF9800 50%, #F57C00 100%);
|
||||
border-radius: 0 0 60rpx 60rpx;
|
||||
background: transparent;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hero-bg::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -80rpx;
|
||||
right: -60rpx;
|
||||
width: 260rpx;
|
||||
height: 260rpx;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 50%;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hero-bg::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -40rpx;
|
||||
left: -40rpx;
|
||||
width: 180rpx;
|
||||
height: 180rpx;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 50%;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hero-pattern {
|
||||
@ -70,7 +55,9 @@
|
||||
/* 比赛头部 */
|
||||
.match-header {
|
||||
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 {
|
||||
@ -80,12 +67,11 @@
|
||||
|
||||
.match-title {
|
||||
display: block;
|
||||
font-size: 44rpx;
|
||||
font-weight: 800;
|
||||
color: #fff;
|
||||
font-size: 48rpx;
|
||||
font-weight: 700;
|
||||
color: #1a1a1a;
|
||||
margin-bottom: 16rpx;
|
||||
text-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
|
||||
letter-spacing: 4rpx;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
.match-status {
|
||||
@ -93,19 +79,18 @@
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
padding: 10rpx 24rpx;
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
background: var(--bg-soft);
|
||||
border-radius: 50rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
backdrop-filter: blur(10px);
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
background: #ff6b35;
|
||||
}
|
||||
|
||||
.match-status.status-1 .status-dot {
|
||||
|
||||
@ -12,19 +12,13 @@
|
||||
/* 顶部装饰背景 */
|
||||
.hero-section {
|
||||
position: relative;
|
||||
padding: 32rpx 24rpx;
|
||||
background: linear-gradient(180deg, #FFF5F0 0%, var(--bg-page) 100%);
|
||||
padding: 48rpx 24rpx 32rpx;
|
||||
background: transparent;
|
||||
border-bottom: 1rpx solid rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.hero-pattern {
|
||||
position: absolute;
|
||||
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;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 积分信息卡片 */
|
||||
@ -34,10 +28,11 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 28rpx 24rpx;
|
||||
padding: 32rpx 28rpx;
|
||||
background: var(--bg-white);
|
||||
border-radius: var(--radius-xl);
|
||||
box-shadow: var(--shadow-lg);
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.06);
|
||||
border: 1rpx solid rgba(0, 0, 0, 0.04);
|
||||
overflow: hidden;
|
||||
animation: fadeInScale 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
@ -47,8 +42,9 @@
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4rpx;
|
||||
height: 3rpx;
|
||||
background: var(--primary-gradient);
|
||||
border-radius: 20rpx 20rpx 0 0;
|
||||
}
|
||||
|
||||
.points-info {
|
||||
|
||||
@ -16,18 +16,12 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 280rpx;
|
||||
background: linear-gradient(180deg, #FFF5F0 0%, var(--bg-page) 100%);
|
||||
background: transparent;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hero-pattern {
|
||||
position: absolute;
|
||||
top: -60rpx;
|
||||
right: -40rpx;
|
||||
width: 220rpx;
|
||||
height: 220rpx;
|
||||
background: radial-gradient(circle, rgba(255, 107, 53, 0.08) 0%, transparent 70%);
|
||||
border-radius: 50%;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 页面标题 */
|
||||
@ -35,16 +29,18 @@
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
padding: 32rpx 24rpx 20rpx;
|
||||
padding: 48rpx 24rpx 32rpx;
|
||||
animation: fadeInUp 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border-bottom: 1rpx solid rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.page-title {
|
||||
display: block;
|
||||
font-size: 36rpx;
|
||||
font-size: 52rpx;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
letter-spacing: 2rpx;
|
||||
color: #1a1a1a;
|
||||
letter-spacing: 1rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
/* 状态筛选标签 */
|
||||
|
||||
@ -15,18 +15,12 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 280rpx;
|
||||
background: linear-gradient(180deg, #FFF5F0 0%, var(--bg-page) 100%);
|
||||
background: transparent;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hero-pattern {
|
||||
position: absolute;
|
||||
top: -60rpx;
|
||||
right: -40rpx;
|
||||
width: 220rpx;
|
||||
height: 220rpx;
|
||||
background: radial-gradient(circle, rgba(255, 107, 53, 0.08) 0%, transparent 70%);
|
||||
border-radius: 50%;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 页面标题 */
|
||||
@ -34,23 +28,26 @@
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
padding: 32rpx 24rpx 24rpx;
|
||||
padding: 48rpx 24rpx 32rpx;
|
||||
animation: fadeInUp 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border-bottom: 1rpx solid rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.page-title {
|
||||
display: block;
|
||||
font-size: 40rpx;
|
||||
font-size: 52rpx;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
color: #1a1a1a;
|
||||
margin-bottom: 8rpx;
|
||||
letter-spacing: 2rpx;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: var(--text-muted);
|
||||
color: #999;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.5rpx;
|
||||
}
|
||||
|
||||
/* 积分概览卡片 */
|
||||
|
||||
@ -15,18 +15,12 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 320rpx;
|
||||
background: linear-gradient(180deg, #FFF5F0 0%, var(--bg-page) 100%);
|
||||
background: transparent;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hero-pattern {
|
||||
position: absolute;
|
||||
top: -80rpx;
|
||||
right: -60rpx;
|
||||
width: 280rpx;
|
||||
height: 280rpx;
|
||||
background: radial-gradient(circle, rgba(255, 107, 53, 0.1) 0%, transparent 70%);
|
||||
border-radius: 50%;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 页面标题 */
|
||||
@ -34,23 +28,26 @@
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
padding: 32rpx 24rpx 24rpx;
|
||||
padding: 48rpx 24rpx 32rpx;
|
||||
animation: fadeInUp 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border-bottom: 1rpx solid rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.page-title {
|
||||
display: block;
|
||||
font-size: 40rpx;
|
||||
font-size: 52rpx;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
color: #1a1a1a;
|
||||
margin-bottom: 8rpx;
|
||||
letter-spacing: 2rpx;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: var(--text-muted);
|
||||
color: #999;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.5rpx;
|
||||
}
|
||||
|
||||
/* 当前门店卡片 */
|
||||
|
||||
@ -64,6 +64,16 @@ Page({
|
||||
|
||||
try {
|
||||
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({
|
||||
userInfo: app.globalData.userInfo,
|
||||
ladderUser: app.globalData.ladderUser,
|
||||
|
||||
@ -115,11 +115,11 @@
|
||||
<text class="record-label">胜场</text>
|
||||
</view>
|
||||
<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>
|
||||
</view>
|
||||
<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>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@ -17,26 +17,19 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 480rpx;
|
||||
background: linear-gradient(180deg, #FFF5F0 0%, var(--bg-page) 100%);
|
||||
background: transparent;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hero-pattern {
|
||||
position: absolute;
|
||||
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;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 用户信息区域 */
|
||||
.user-section {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 32rpx 24rpx 24rpx;
|
||||
padding: 48rpx 24rpx 24rpx;
|
||||
}
|
||||
|
||||
/* ==========================================
|
||||
@ -46,11 +39,12 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 48rpx 32rpx 40rpx;
|
||||
padding: 52rpx 32rpx 44rpx;
|
||||
background: var(--bg-white);
|
||||
border-radius: var(--radius-xl);
|
||||
box-shadow: var(--shadow-lg);
|
||||
margin-bottom: 20rpx;
|
||||
border-radius: 24rpx;
|
||||
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.06);
|
||||
margin-bottom: 24rpx;
|
||||
border: 1rpx solid rgba(0, 0, 0, 0.04);
|
||||
animation: fadeInScale 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
|
||||
@ -16,6 +16,17 @@ NODE_ENV=development
|
||||
# 服务端口(默认3000)
|
||||
PORT=3000
|
||||
|
||||
# 基础URL(用于生成完整的文件URL,支持HTTPS)
|
||||
# 示例:https://api.example.com 或 http://localhost:3000
|
||||
# 如果设置了此项,上传接口返回的URL将使用此值
|
||||
# 如果不设置,将根据请求自动检测协议(支持反向代理)
|
||||
BASE_URL=
|
||||
|
||||
# 强制使用HTTPS(true/false)
|
||||
# 如果设置为 true,所有返回的URL将使用 https://
|
||||
# 适用于部署在反向代理(如nginx)后面的情况
|
||||
FORCE_HTTPS=false
|
||||
|
||||
# ------------------------------------------
|
||||
# 数据库配置 (MySQL)
|
||||
# ------------------------------------------
|
||||
|
||||
@ -6,7 +6,8 @@
|
||||
"scripts": {
|
||||
"start": "node 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": {
|
||||
"express": "^4.18.2",
|
||||
|
||||
@ -18,6 +18,10 @@ const uploadRoutes = require('./routes/upload');
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
|
||||
// 信任代理(支持反向代理的 HTTPS)
|
||||
// 如果应用部署在 nginx 等反向代理后面,需要设置此项
|
||||
app.set('trust proxy', true);
|
||||
|
||||
// 中间件
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
@ -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) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
@ -114,6 +114,42 @@ class MatchController {
|
||||
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({
|
||||
store_id,
|
||||
@ -184,6 +220,27 @@ class MatchController {
|
||||
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) {
|
||||
// 接受挑战
|
||||
await match.update({ status: MATCH_STATUS.ONGOING, start_time: new Date() }, { transaction: t });
|
||||
@ -901,6 +958,7 @@ class MatchController {
|
||||
async getMatchDetail(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const user = req.user;
|
||||
|
||||
const match = await Match.findByPk(id, {
|
||||
include: [
|
||||
@ -913,7 +971,179 @@ class MatchController {
|
||||
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,
|
||||
matchCode: match.match_code,
|
||||
type: match.type,
|
||||
@ -924,8 +1154,49 @@ class MatchController {
|
||||
storeName: match.store?.name,
|
||||
startTime: match.start_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) {
|
||||
console.error('获取比赛详情失败:', err);
|
||||
res.status(500).json(error('获取失败'));
|
||||
|
||||
@ -128,8 +128,28 @@ class PointsAdminController {
|
||||
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) {
|
||||
await t.rollback();
|
||||
return res.status(404).json(error('用户不存在', 404));
|
||||
|
||||
@ -14,6 +14,7 @@ router.get('/dashboard', authAdmin, adminController.getDashboard);
|
||||
|
||||
// === 用户管理(超管) ===
|
||||
router.get('/users', authAdmin, requireSuperAdmin, adminController.getUsers);
|
||||
router.get('/users/search', authAdmin, adminController.searchUsers); // 搜索用户(用于积分操作,普通管理员可用)
|
||||
router.get('/users/:id', authAdmin, requireSuperAdmin, adminController.getUserDetail);
|
||||
router.put('/users/:id/status', authAdmin, requireSuperAdmin, adminController.updateUserStatus);
|
||||
|
||||
|
||||
@ -41,8 +41,9 @@ router.post('/image', authAdmin, upload.single('file'), (req, res) => {
|
||||
return res.status(400).json(error('请选择要上传的图片'));
|
||||
}
|
||||
|
||||
const url = `/uploads/${req.file.filename}`;
|
||||
res.json(success({ url }, '上传成功'));
|
||||
const relativePath = `/uploads/${req.file.filename}`;
|
||||
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('请选择要上传的图片'));
|
||||
}
|
||||
|
||||
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 }, '上传成功'));
|
||||
});
|
||||
|
||||
|
||||
168
server/src/scripts/generateMockLadderData.js
Normal file
168
server/src/scripts/generateMockLadderData.js
Normal 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();
|
||||
@ -103,9 +103,45 @@ function getFullUrl(path, req) {
|
||||
if (path.startsWith('http://') || path.startsWith('https://')) {
|
||||
return path;
|
||||
}
|
||||
|
||||
// 相对路径,拼接服务器地址
|
||||
const protocol = req?.protocol || 'http';
|
||||
const host = req?.get('host') || `localhost:${process.env.PORT || 3000}`;
|
||||
// 支持反向代理(nginx等)的 HTTPS 检测
|
||||
let protocol = 'http';
|
||||
if (req) {
|
||||
// 优先使用 X-Forwarded-Proto 头(反向代理设置)
|
||||
const forwardedProto = req.get('X-Forwarded-Proto');
|
||||
if (forwardedProto) {
|
||||
protocol = forwardedProto;
|
||||
} else {
|
||||
// 使用 req.protocol(Express 会自动处理)
|
||||
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}`;
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user