feat(天梯): 新增选手定位功能并调整挑战赛权重
- 在小程序天梯排名页添加“定位我”按钮,点击可滚动到当前用户所在位置 - 新增获取用户排名接口 `/ladder/my-rank` 用于定位计算 - 调整挑战赛权重从 1.5 降至 1.0,与日常畅打保持一致 - 新增数据库脚本 `setChallengeMatchWeightTo1.js` 用于更新历史数据 - 在管理员界面创建天梯用户时,根据所选等级自动填充默认战力值 - 修复管理员更新比赛时挑战赛权重强制设置为 1.0 的问题 - 新增天梯汇总大屏页面及相关路由 - 添加大屏比赛列表接口 `/match/display-list` 用于展示进行中和近期比赛 - 优化用户详情页的胜负场和胜率显示逻辑 - 修复小程序用户注册时的性别选择逻辑
This commit is contained in:
parent
d3f0515155
commit
02937ca33c
4
admin/node_modules/.vite/deps/@element-plus_icons-vue.js
generated
vendored
4
admin/node_modules/.vite/deps/@element-plus_icons-vue.js
generated
vendored
@ -292,8 +292,8 @@ import {
|
||||
wind_power_default,
|
||||
zoom_in_default,
|
||||
zoom_out_default
|
||||
} from "./chunk-L7WLSQ4R.js";
|
||||
import "./chunk-ELEEJBJQ.js";
|
||||
} from "./chunk-OP4ZUAFM.js";
|
||||
import "./chunk-H2732BJL.js";
|
||||
import "./chunk-G3PMV62Z.js";
|
||||
export {
|
||||
add_location_default as AddLocation,
|
||||
|
||||
324
admin/node_modules/.vite/deps/_metadata.json
generated
vendored
324
admin/node_modules/.vite/deps/_metadata.json
generated
vendored
@ -1,358 +1,346 @@
|
||||
{
|
||||
"hash": "02b9d14d",
|
||||
"configHash": "502f42c3",
|
||||
"hash": "0b0fcdca",
|
||||
"configHash": "0bd4dba1",
|
||||
"lockfileHash": "45a4e0fd",
|
||||
"browserHash": "e5c88585",
|
||||
"browserHash": "2a9cc92b",
|
||||
"optimized": {
|
||||
"@element-plus/icons-vue": {
|
||||
"src": "../../@element-plus/icons-vue/dist/index.js",
|
||||
"file": "@element-plus_icons-vue.js",
|
||||
"fileHash": "4df6a1b3",
|
||||
"fileHash": "0ce2075a",
|
||||
"needsInterop": false
|
||||
},
|
||||
"axios": {
|
||||
"src": "../../axios/index.js",
|
||||
"file": "axios.js",
|
||||
"fileHash": "43b194e5",
|
||||
"fileHash": "e0339058",
|
||||
"needsInterop": false
|
||||
},
|
||||
"dayjs": {
|
||||
"src": "../../dayjs/dayjs.min.js",
|
||||
"file": "dayjs.js",
|
||||
"fileHash": "ee47ac98",
|
||||
"fileHash": "d6bd156a",
|
||||
"needsInterop": true
|
||||
},
|
||||
"element-plus": {
|
||||
"src": "../../element-plus/es/index.mjs",
|
||||
"file": "element-plus.js",
|
||||
"fileHash": "a2307ed3",
|
||||
"fileHash": "eb6d080f",
|
||||
"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": "ab38f2b9",
|
||||
"fileHash": "23bb7a06",
|
||||
"needsInterop": false
|
||||
},
|
||||
"pinia": {
|
||||
"src": "../../pinia/dist/pinia.mjs",
|
||||
"file": "pinia.js",
|
||||
"fileHash": "3fe9af82",
|
||||
"fileHash": "3e06a4e4",
|
||||
"needsInterop": false
|
||||
},
|
||||
"qrcode": {
|
||||
"src": "../../qrcode/lib/browser.js",
|
||||
"file": "qrcode.js",
|
||||
"fileHash": "c89f7311",
|
||||
"fileHash": "b1dc1626",
|
||||
"needsInterop": true
|
||||
},
|
||||
"vue": {
|
||||
"src": "../../vue/dist/vue.runtime.esm-bundler.js",
|
||||
"file": "vue.js",
|
||||
"fileHash": "0d9f3b0d",
|
||||
"fileHash": "cf2fc702",
|
||||
"needsInterop": false
|
||||
},
|
||||
"vue-router": {
|
||||
"src": "../../vue-router/dist/vue-router.mjs",
|
||||
"file": "vue-router.js",
|
||||
"fileHash": "a57ec867",
|
||||
"fileHash": "731df609",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es": {
|
||||
"src": "../../element-plus/es/index.mjs",
|
||||
"file": "element-plus_es.js",
|
||||
"fileHash": "ffb2b783",
|
||||
"fileHash": "a8df203a",
|
||||
"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": "3a6cacdf",
|
||||
"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": "da37a236",
|
||||
"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": "eb41d819",
|
||||
"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": "b7497adf",
|
||||
"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": "5da3bed7",
|
||||
"fileHash": "a217b400",
|
||||
"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": "06dd3372",
|
||||
"fileHash": "75a45c45",
|
||||
"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": "d80bcc5a",
|
||||
"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": "1c8a603a",
|
||||
"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": "0acbe0b6",
|
||||
"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": "7206bf43",
|
||||
"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": "6d1afd0d",
|
||||
"fileHash": "98a7fc8d",
|
||||
"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": "240bd7ee",
|
||||
"fileHash": "06ad125e",
|
||||
"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": "41bb0592",
|
||||
"fileHash": "0d145a75",
|
||||
"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": "6969c6d1",
|
||||
"fileHash": "59eef490",
|
||||
"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": "825caf98",
|
||||
"fileHash": "14e34d1b",
|
||||
"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": "ff1984bb",
|
||||
"fileHash": "b27e43bb",
|
||||
"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": "4b4a4a1d",
|
||||
"fileHash": "2a5e4249",
|
||||
"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": "3c495fad",
|
||||
"fileHash": "429623db",
|
||||
"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": "3d5b413c",
|
||||
"fileHash": "41266817",
|
||||
"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": "7b9f21f2",
|
||||
"fileHash": "7d5a15fd",
|
||||
"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": "bf30c0ea",
|
||||
"fileHash": "16c1dd4d",
|
||||
"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": "9d2c02ba",
|
||||
"fileHash": "61715a00",
|
||||
"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": "3758817f",
|
||||
"fileHash": "dadfdc54",
|
||||
"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": "c0aa2e6f",
|
||||
"fileHash": "fe1a5065",
|
||||
"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": "e2cec4e0",
|
||||
"fileHash": "18633b2b",
|
||||
"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": "df646624",
|
||||
"fileHash": "9560cfd1",
|
||||
"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": "ee0b3ee3",
|
||||
"fileHash": "28bb8b75",
|
||||
"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": "3912ea08",
|
||||
"fileHash": "e29b5cd8",
|
||||
"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": "c7565ad8",
|
||||
"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": "0fdf620f",
|
||||
"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": "957ebe8c",
|
||||
"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": "78632d60",
|
||||
"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": "fbf0885c",
|
||||
"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": "a3645e7c",
|
||||
"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": "1643aa99",
|
||||
"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": "8d67fb6e",
|
||||
"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": "a3326872",
|
||||
"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": "348ec1fe",
|
||||
"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": "f4e21a7c",
|
||||
"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": "e8cb2b84",
|
||||
"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": "86b9c070",
|
||||
"fileHash": "c5141ba1",
|
||||
"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": "f73dd83d",
|
||||
"fileHash": "37c51f12",
|
||||
"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": "23ae7ef7",
|
||||
"fileHash": "e5b97e77",
|
||||
"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": "2b98b2df",
|
||||
"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": "a90022be",
|
||||
"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": "42b918e0",
|
||||
"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": "29d647a3",
|
||||
"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": "1f1a4f40",
|
||||
"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": "f35fe942",
|
||||
"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": "7ee42f75",
|
||||
"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": "78e3b880",
|
||||
"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": "585f05ee",
|
||||
"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": "0dc4fea5",
|
||||
"needsInterop": false
|
||||
}
|
||||
},
|
||||
"chunks": {
|
||||
"chunk-LZQ3XESL": {
|
||||
"file": "chunk-LZQ3XESL.js"
|
||||
"chunk-4PW274X2": {
|
||||
"file": "chunk-4PW274X2.js"
|
||||
},
|
||||
"chunk-SB5RV2IU": {
|
||||
"file": "chunk-SB5RV2IU.js"
|
||||
"chunk-B2YDYSZR": {
|
||||
"file": "chunk-B2YDYSZR.js"
|
||||
},
|
||||
"chunk-JJYIKYUB": {
|
||||
"file": "chunk-JJYIKYUB.js"
|
||||
"chunk-75C4BP7B": {
|
||||
"file": "chunk-75C4BP7B.js"
|
||||
},
|
||||
"chunk-P5E57UKJ": {
|
||||
"file": "chunk-P5E57UKJ.js"
|
||||
"chunk-UBLR4G7Q": {
|
||||
"file": "chunk-UBLR4G7Q.js"
|
||||
},
|
||||
"chunk-62JQAJRX": {
|
||||
"file": "chunk-62JQAJRX.js"
|
||||
"chunk-5KK3TTMN": {
|
||||
"file": "chunk-5KK3TTMN.js"
|
||||
},
|
||||
"chunk-3Y6T2BWZ": {
|
||||
"file": "chunk-3Y6T2BWZ.js"
|
||||
"chunk-R5DNQ3QC": {
|
||||
"file": "chunk-R5DNQ3QC.js"
|
||||
},
|
||||
"chunk-ZCVI6XFC": {
|
||||
"file": "chunk-ZCVI6XFC.js"
|
||||
"chunk-NKQWFVTF": {
|
||||
"file": "chunk-NKQWFVTF.js"
|
||||
},
|
||||
"chunk-MGVC5NZO": {
|
||||
"file": "chunk-MGVC5NZO.js"
|
||||
"chunk-REWOA3VH": {
|
||||
"file": "chunk-REWOA3VH.js"
|
||||
},
|
||||
"chunk-2XFUNMCG": {
|
||||
"file": "chunk-2XFUNMCG.js"
|
||||
"chunk-TX5YLZ4O": {
|
||||
"file": "chunk-TX5YLZ4O.js"
|
||||
},
|
||||
"chunk-VID4RN2V": {
|
||||
"file": "chunk-VID4RN2V.js"
|
||||
"chunk-SMFPDFTD": {
|
||||
"file": "chunk-SMFPDFTD.js"
|
||||
},
|
||||
"chunk-BXHXCFF5": {
|
||||
"file": "chunk-BXHXCFF5.js"
|
||||
"chunk-JUCAMQ7P": {
|
||||
"file": "chunk-JUCAMQ7P.js"
|
||||
},
|
||||
"chunk-TBYZ47XG": {
|
||||
"file": "chunk-TBYZ47XG.js"
|
||||
"chunk-IV6PSERC": {
|
||||
"file": "chunk-IV6PSERC.js"
|
||||
},
|
||||
"chunk-WNNLDN6V": {
|
||||
"file": "chunk-WNNLDN6V.js"
|
||||
"chunk-6CKQ2YFZ": {
|
||||
"file": "chunk-6CKQ2YFZ.js"
|
||||
},
|
||||
"chunk-WDY4WTIC": {
|
||||
"file": "chunk-WDY4WTIC.js"
|
||||
},
|
||||
"chunk-L7WLSQ4R": {
|
||||
"file": "chunk-L7WLSQ4R.js"
|
||||
},
|
||||
"chunk-YAGW2SQC": {
|
||||
"file": "chunk-YAGW2SQC.js"
|
||||
"chunk-OP4ZUAFM": {
|
||||
"file": "chunk-OP4ZUAFM.js"
|
||||
},
|
||||
"chunk-QZC7O2C6": {
|
||||
"file": "chunk-QZC7O2C6.js"
|
||||
},
|
||||
"chunk-ELEEJBJQ": {
|
||||
"file": "chunk-ELEEJBJQ.js"
|
||||
"chunk-YFT6OQ5R": {
|
||||
"file": "chunk-YFT6OQ5R.js"
|
||||
},
|
||||
"chunk-HYZ2CRGS": {
|
||||
"file": "chunk-HYZ2CRGS.js"
|
||||
},
|
||||
"chunk-H2732BJL": {
|
||||
"file": "chunk-H2732BJL.js"
|
||||
},
|
||||
"chunk-G3PMV62Z": {
|
||||
"file": "chunk-G3PMV62Z.js"
|
||||
|
||||
2
admin/node_modules/.vite/deps/axios.js.map
generated
vendored
2
admin/node_modules/.vite/deps/axios.js.map
generated
vendored
File diff suppressed because one or more lines are too long
8
admin/node_modules/.vite/deps/element-plus.js
generated
vendored
8
admin/node_modules/.vite/deps/element-plus.js
generated
vendored
@ -514,11 +514,11 @@ import {
|
||||
virtualizedScrollbarProps,
|
||||
watermarkProps,
|
||||
zIndexContextKey
|
||||
} from "./chunk-WDY4WTIC.js";
|
||||
import "./chunk-L7WLSQ4R.js";
|
||||
import "./chunk-YAGW2SQC.js";
|
||||
} from "./chunk-6CKQ2YFZ.js";
|
||||
import "./chunk-OP4ZUAFM.js";
|
||||
import "./chunk-QZC7O2C6.js";
|
||||
import "./chunk-ELEEJBJQ.js";
|
||||
import "./chunk-HYZ2CRGS.js";
|
||||
import "./chunk-H2732BJL.js";
|
||||
import "./chunk-G3PMV62Z.js";
|
||||
var export_dayjs = import_dayjs.default;
|
||||
export {
|
||||
|
||||
2
admin/node_modules/.vite/deps/element-plus_dist_locale_zh-cn__mjs.js.map
generated
vendored
2
admin/node_modules/.vite/deps/element-plus_dist_locale_zh-cn__mjs.js.map
generated
vendored
File diff suppressed because one or more lines are too long
8
admin/node_modules/.vite/deps/element-plus_es.js
generated
vendored
8
admin/node_modules/.vite/deps/element-plus_es.js
generated
vendored
@ -514,11 +514,11 @@ import {
|
||||
virtualizedScrollbarProps,
|
||||
watermarkProps,
|
||||
zIndexContextKey
|
||||
} from "./chunk-WDY4WTIC.js";
|
||||
import "./chunk-L7WLSQ4R.js";
|
||||
import "./chunk-YAGW2SQC.js";
|
||||
} from "./chunk-6CKQ2YFZ.js";
|
||||
import "./chunk-OP4ZUAFM.js";
|
||||
import "./chunk-QZC7O2C6.js";
|
||||
import "./chunk-ELEEJBJQ.js";
|
||||
import "./chunk-HYZ2CRGS.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_aside_style_css.js
generated
vendored
4
admin/node_modules/.vite/deps/element-plus_es_components_aside_style_css.js
generated
vendored
@ -1,5 +1,5 @@
|
||||
import "./chunk-WNNLDN6V.js";
|
||||
import "./chunk-IV6PSERC.js";
|
||||
|
||||
// node_modules/element-plus/es/components/aside/style/css.mjs
|
||||
import "E:/workspace/yingsa/admin/node_modules/element-plus/theme-chalk/el-aside.css";
|
||||
import "E:/workspace/yingsha/admin/node_modules/element-plus/theme-chalk/el-aside.css";
|
||||
//# sourceMappingURL=element-plus_es_components_aside_style_css.js.map
|
||||
|
||||
2
admin/node_modules/.vite/deps/element-plus_es_components_aside_style_css.js.map
generated
vendored
2
admin/node_modules/.vite/deps/element-plus_es_components_aside_style_css.js.map
generated
vendored
@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../element-plus/es/components/aside/style/css.mjs"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\r\nimport 'element-plus/theme-chalk/el-aside.css';\r\n//# sourceMappingURL=css.mjs.map\r\n"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\nimport 'element-plus/theme-chalk/el-aside.css';\n//# sourceMappingURL=css.mjs.map\n"],
|
||||
"mappings": ";;;AACA,OAAO;",
|
||||
"names": []
|
||||
}
|
||||
|
||||
4
admin/node_modules/.vite/deps/element-plus_es_components_avatar_style_css.js
generated
vendored
4
admin/node_modules/.vite/deps/element-plus_es_components_avatar_style_css.js
generated
vendored
@ -1,5 +1,5 @@
|
||||
import "./chunk-WNNLDN6V.js";
|
||||
import "./chunk-IV6PSERC.js";
|
||||
|
||||
// node_modules/element-plus/es/components/avatar/style/css.mjs
|
||||
import "E:/workspace/yingsa/admin/node_modules/element-plus/theme-chalk/el-avatar.css";
|
||||
import "E:/workspace/yingsha/admin/node_modules/element-plus/theme-chalk/el-avatar.css";
|
||||
//# sourceMappingURL=element-plus_es_components_avatar_style_css.js.map
|
||||
|
||||
2
admin/node_modules/.vite/deps/element-plus_es_components_avatar_style_css.js.map
generated
vendored
2
admin/node_modules/.vite/deps/element-plus_es_components_avatar_style_css.js.map
generated
vendored
@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../element-plus/es/components/avatar/style/css.mjs"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\r\nimport 'element-plus/theme-chalk/el-avatar.css';\r\n//# sourceMappingURL=css.mjs.map\r\n"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\nimport 'element-plus/theme-chalk/el-avatar.css';\n//# sourceMappingURL=css.mjs.map\n"],
|
||||
"mappings": ";;;AACA,OAAO;",
|
||||
"names": []
|
||||
}
|
||||
|
||||
2
admin/node_modules/.vite/deps/element-plus_es_components_base_style_css.js
generated
vendored
2
admin/node_modules/.vite/deps/element-plus_es_components_base_style_css.js
generated
vendored
@ -1,2 +1,2 @@
|
||||
import "./chunk-WNNLDN6V.js";
|
||||
import "./chunk-IV6PSERC.js";
|
||||
//# sourceMappingURL=element-plus_es_components_base_style_css.js.map
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import "./chunk-WNNLDN6V.js";
|
||||
import "./chunk-IV6PSERC.js";
|
||||
|
||||
// node_modules/element-plus/es/components/breadcrumb-item/style/css.mjs
|
||||
import "E:/workspace/yingsa/admin/node_modules/element-plus/theme-chalk/el-breadcrumb-item.css";
|
||||
import "E:/workspace/yingsha/admin/node_modules/element-plus/theme-chalk/el-breadcrumb-item.css";
|
||||
//# sourceMappingURL=element-plus_es_components_breadcrumb-item_style_css.js.map
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../element-plus/es/components/breadcrumb-item/style/css.mjs"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\r\nimport 'element-plus/theme-chalk/el-breadcrumb-item.css';\r\n//# sourceMappingURL=css.mjs.map\r\n"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\nimport 'element-plus/theme-chalk/el-breadcrumb-item.css';\n//# sourceMappingURL=css.mjs.map\n"],
|
||||
"mappings": ";;;AACA,OAAO;",
|
||||
"names": []
|
||||
}
|
||||
|
||||
4
admin/node_modules/.vite/deps/element-plus_es_components_breadcrumb_style_css.js
generated
vendored
4
admin/node_modules/.vite/deps/element-plus_es_components_breadcrumb_style_css.js
generated
vendored
@ -1,5 +1,5 @@
|
||||
import "./chunk-WNNLDN6V.js";
|
||||
import "./chunk-IV6PSERC.js";
|
||||
|
||||
// node_modules/element-plus/es/components/breadcrumb/style/css.mjs
|
||||
import "E:/workspace/yingsa/admin/node_modules/element-plus/theme-chalk/el-breadcrumb.css";
|
||||
import "E:/workspace/yingsha/admin/node_modules/element-plus/theme-chalk/el-breadcrumb.css";
|
||||
//# sourceMappingURL=element-plus_es_components_breadcrumb_style_css.js.map
|
||||
|
||||
2
admin/node_modules/.vite/deps/element-plus_es_components_breadcrumb_style_css.js.map
generated
vendored
2
admin/node_modules/.vite/deps/element-plus_es_components_breadcrumb_style_css.js.map
generated
vendored
@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../element-plus/es/components/breadcrumb/style/css.mjs"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\r\nimport 'element-plus/theme-chalk/el-breadcrumb.css';\r\n//# sourceMappingURL=css.mjs.map\r\n"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\nimport 'element-plus/theme-chalk/el-breadcrumb.css';\n//# sourceMappingURL=css.mjs.map\n"],
|
||||
"mappings": ";;;AACA,OAAO;",
|
||||
"names": []
|
||||
}
|
||||
|
||||
4
admin/node_modules/.vite/deps/element-plus_es_components_button_style_css.js
generated
vendored
4
admin/node_modules/.vite/deps/element-plus_es_components_button_style_css.js
generated
vendored
@ -1,3 +1,3 @@
|
||||
import "./chunk-BXHXCFF5.js";
|
||||
import "./chunk-WNNLDN6V.js";
|
||||
import "./chunk-SMFPDFTD.js";
|
||||
import "./chunk-IV6PSERC.js";
|
||||
//# sourceMappingURL=element-plus_es_components_button_style_css.js.map
|
||||
|
||||
4
admin/node_modules/.vite/deps/element-plus_es_components_col_style_css.js
generated
vendored
4
admin/node_modules/.vite/deps/element-plus_es_components_col_style_css.js
generated
vendored
@ -1,5 +1,5 @@
|
||||
import "./chunk-WNNLDN6V.js";
|
||||
import "./chunk-IV6PSERC.js";
|
||||
|
||||
// node_modules/element-plus/es/components/col/style/css.mjs
|
||||
import "E:/workspace/yingsa/admin/node_modules/element-plus/theme-chalk/el-col.css";
|
||||
import "E:/workspace/yingsha/admin/node_modules/element-plus/theme-chalk/el-col.css";
|
||||
//# sourceMappingURL=element-plus_es_components_col_style_css.js.map
|
||||
|
||||
2
admin/node_modules/.vite/deps/element-plus_es_components_col_style_css.js.map
generated
vendored
2
admin/node_modules/.vite/deps/element-plus_es_components_col_style_css.js.map
generated
vendored
@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../element-plus/es/components/col/style/css.mjs"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\r\nimport 'element-plus/theme-chalk/el-col.css';\r\n//# sourceMappingURL=css.mjs.map\r\n"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\nimport 'element-plus/theme-chalk/el-col.css';\n//# sourceMappingURL=css.mjs.map\n"],
|
||||
"mappings": ";;;AACA,OAAO;",
|
||||
"names": []
|
||||
}
|
||||
|
||||
12
admin/node_modules/.vite/deps/element-plus_es_components_container_style_css.js
generated
vendored
12
admin/node_modules/.vite/deps/element-plus_es_components_container_style_css.js
generated
vendored
@ -1,9 +1,9 @@
|
||||
import "./chunk-WNNLDN6V.js";
|
||||
import "./chunk-IV6PSERC.js";
|
||||
|
||||
// node_modules/element-plus/es/components/container/style/css.mjs
|
||||
import "E:/workspace/yingsa/admin/node_modules/element-plus/theme-chalk/el-container.css";
|
||||
import "E:/workspace/yingsa/admin/node_modules/element-plus/theme-chalk/el-aside.css";
|
||||
import "E:/workspace/yingsa/admin/node_modules/element-plus/theme-chalk/el-footer.css";
|
||||
import "E:/workspace/yingsa/admin/node_modules/element-plus/theme-chalk/el-header.css";
|
||||
import "E:/workspace/yingsa/admin/node_modules/element-plus/theme-chalk/el-main.css";
|
||||
import "E:/workspace/yingsha/admin/node_modules/element-plus/theme-chalk/el-container.css";
|
||||
import "E:/workspace/yingsha/admin/node_modules/element-plus/theme-chalk/el-aside.css";
|
||||
import "E:/workspace/yingsha/admin/node_modules/element-plus/theme-chalk/el-footer.css";
|
||||
import "E:/workspace/yingsha/admin/node_modules/element-plus/theme-chalk/el-header.css";
|
||||
import "E:/workspace/yingsha/admin/node_modules/element-plus/theme-chalk/el-main.css";
|
||||
//# sourceMappingURL=element-plus_es_components_container_style_css.js.map
|
||||
|
||||
2
admin/node_modules/.vite/deps/element-plus_es_components_container_style_css.js.map
generated
vendored
2
admin/node_modules/.vite/deps/element-plus_es_components_container_style_css.js.map
generated
vendored
@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../element-plus/es/components/container/style/css.mjs"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\r\nimport 'element-plus/theme-chalk/el-container.css';\r\nimport 'element-plus/theme-chalk/el-aside.css';\r\nimport 'element-plus/theme-chalk/el-footer.css';\r\nimport 'element-plus/theme-chalk/el-header.css';\r\nimport 'element-plus/theme-chalk/el-main.css';\r\n//# sourceMappingURL=css.mjs.map\r\n"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\nimport 'element-plus/theme-chalk/el-container.css';\nimport 'element-plus/theme-chalk/el-aside.css';\nimport 'element-plus/theme-chalk/el-footer.css';\nimport 'element-plus/theme-chalk/el-header.css';\nimport 'element-plus/theme-chalk/el-main.css';\n//# sourceMappingURL=css.mjs.map\n"],
|
||||
"mappings": ";;;AACA,OAAO;AACP,OAAO;AACP,OAAO;AACP,OAAO;AACP,OAAO;",
|
||||
"names": []
|
||||
}
|
||||
|
||||
6
admin/node_modules/.vite/deps/element-plus_es_components_dialog_style_css.js
generated
vendored
6
admin/node_modules/.vite/deps/element-plus_es_components_dialog_style_css.js
generated
vendored
@ -1,8 +1,8 @@
|
||||
import "./chunk-WNNLDN6V.js";
|
||||
import "./chunk-IV6PSERC.js";
|
||||
|
||||
// node_modules/element-plus/es/components/dialog/style/css.mjs
|
||||
import "E:/workspace/yingsa/admin/node_modules/element-plus/theme-chalk/el-dialog.css";
|
||||
import "E:/workspace/yingsha/admin/node_modules/element-plus/theme-chalk/el-dialog.css";
|
||||
|
||||
// node_modules/element-plus/es/components/overlay/style/css.mjs
|
||||
import "E:/workspace/yingsa/admin/node_modules/element-plus/theme-chalk/el-overlay.css";
|
||||
import "E:/workspace/yingsha/admin/node_modules/element-plus/theme-chalk/el-overlay.css";
|
||||
//# sourceMappingURL=element-plus_es_components_dialog_style_css.js.map
|
||||
|
||||
2
admin/node_modules/.vite/deps/element-plus_es_components_dialog_style_css.js.map
generated
vendored
2
admin/node_modules/.vite/deps/element-plus_es_components_dialog_style_css.js.map
generated
vendored
@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../element-plus/es/components/dialog/style/css.mjs", "../../element-plus/es/components/overlay/style/css.mjs"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\r\nimport 'element-plus/theme-chalk/el-dialog.css';\r\nimport '../../overlay/style/css.mjs';\r\n//# sourceMappingURL=css.mjs.map\r\n", "import '../../base/style/css.mjs';\r\nimport 'element-plus/theme-chalk/el-overlay.css';\r\n//# sourceMappingURL=css.mjs.map\r\n"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\nimport 'element-plus/theme-chalk/el-dialog.css';\nimport '../../overlay/style/css.mjs';\n//# sourceMappingURL=css.mjs.map\n", "import '../../base/style/css.mjs';\nimport 'element-plus/theme-chalk/el-overlay.css';\n//# sourceMappingURL=css.mjs.map\n"],
|
||||
"mappings": ";;;AACA,OAAO;;;ACAP,OAAO;",
|
||||
"names": []
|
||||
}
|
||||
|
||||
4
admin/node_modules/.vite/deps/element-plus_es_components_dropdown-item_style_css.js
generated
vendored
4
admin/node_modules/.vite/deps/element-plus_es_components_dropdown-item_style_css.js
generated
vendored
@ -1,5 +1,5 @@
|
||||
import "./chunk-WNNLDN6V.js";
|
||||
import "./chunk-IV6PSERC.js";
|
||||
|
||||
// node_modules/element-plus/es/components/dropdown-item/style/css.mjs
|
||||
import "E:/workspace/yingsa/admin/node_modules/element-plus/theme-chalk/el-dropdown-item.css";
|
||||
import "E:/workspace/yingsha/admin/node_modules/element-plus/theme-chalk/el-dropdown-item.css";
|
||||
//# sourceMappingURL=element-plus_es_components_dropdown-item_style_css.js.map
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../element-plus/es/components/dropdown-item/style/css.mjs"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\r\nimport 'element-plus/theme-chalk/el-dropdown-item.css';\r\n//# sourceMappingURL=css.mjs.map\r\n"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\nimport 'element-plus/theme-chalk/el-dropdown-item.css';\n//# sourceMappingURL=css.mjs.map\n"],
|
||||
"mappings": ";;;AACA,OAAO;",
|
||||
"names": []
|
||||
}
|
||||
|
||||
4
admin/node_modules/.vite/deps/element-plus_es_components_dropdown-menu_style_css.js
generated
vendored
4
admin/node_modules/.vite/deps/element-plus_es_components_dropdown-menu_style_css.js
generated
vendored
@ -1,5 +1,5 @@
|
||||
import "./chunk-WNNLDN6V.js";
|
||||
import "./chunk-IV6PSERC.js";
|
||||
|
||||
// node_modules/element-plus/es/components/dropdown-menu/style/css.mjs
|
||||
import "E:/workspace/yingsa/admin/node_modules/element-plus/theme-chalk/el-dropdown-menu.css";
|
||||
import "E:/workspace/yingsha/admin/node_modules/element-plus/theme-chalk/el-dropdown-menu.css";
|
||||
//# sourceMappingURL=element-plus_es_components_dropdown-menu_style_css.js.map
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../element-plus/es/components/dropdown-menu/style/css.mjs"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\r\nimport 'element-plus/theme-chalk/el-dropdown-menu.css';\r\n//# sourceMappingURL=css.mjs.map\r\n"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\nimport 'element-plus/theme-chalk/el-dropdown-menu.css';\n//# sourceMappingURL=css.mjs.map\n"],
|
||||
"mappings": ";;;AACA,OAAO;",
|
||||
"names": []
|
||||
}
|
||||
|
||||
12
admin/node_modules/.vite/deps/element-plus_es_components_dropdown_style_css.js
generated
vendored
12
admin/node_modules/.vite/deps/element-plus_es_components_dropdown_style_css.js
generated
vendored
@ -1,11 +1,11 @@
|
||||
import "./chunk-MGVC5NZO.js";
|
||||
import "./chunk-2XFUNMCG.js";
|
||||
import "./chunk-BXHXCFF5.js";
|
||||
import "./chunk-WNNLDN6V.js";
|
||||
import "./chunk-REWOA3VH.js";
|
||||
import "./chunk-TX5YLZ4O.js";
|
||||
import "./chunk-SMFPDFTD.js";
|
||||
import "./chunk-IV6PSERC.js";
|
||||
|
||||
// node_modules/element-plus/es/components/button-group/style/css.mjs
|
||||
import "E:/workspace/yingsa/admin/node_modules/element-plus/theme-chalk/el-button-group.css";
|
||||
import "E:/workspace/yingsha/admin/node_modules/element-plus/theme-chalk/el-button-group.css";
|
||||
|
||||
// node_modules/element-plus/es/components/dropdown/style/css.mjs
|
||||
import "E:/workspace/yingsa/admin/node_modules/element-plus/theme-chalk/el-dropdown.css";
|
||||
import "E:/workspace/yingsha/admin/node_modules/element-plus/theme-chalk/el-dropdown.css";
|
||||
//# sourceMappingURL=element-plus_es_components_dropdown_style_css.js.map
|
||||
|
||||
2
admin/node_modules/.vite/deps/element-plus_es_components_dropdown_style_css.js.map
generated
vendored
2
admin/node_modules/.vite/deps/element-plus_es_components_dropdown_style_css.js.map
generated
vendored
@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../element-plus/es/components/button-group/style/css.mjs", "../../element-plus/es/components/dropdown/style/css.mjs"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\r\nimport 'element-plus/theme-chalk/el-button-group.css';\r\n//# sourceMappingURL=css.mjs.map\r\n", "import '../../base/style/css.mjs';\r\nimport '../../button/style/css.mjs';\r\nimport '../../button-group/style/css.mjs';\r\nimport '../../popper/style/css.mjs';\r\nimport '../../scrollbar/style/css.mjs';\r\nimport 'element-plus/theme-chalk/el-dropdown.css';\r\n//# sourceMappingURL=css.mjs.map\r\n"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\nimport 'element-plus/theme-chalk/el-button-group.css';\n//# sourceMappingURL=css.mjs.map\n", "import '../../base/style/css.mjs';\nimport '../../button/style/css.mjs';\nimport '../../button-group/style/css.mjs';\nimport '../../popper/style/css.mjs';\nimport '../../scrollbar/style/css.mjs';\nimport 'element-plus/theme-chalk/el-dropdown.css';\n//# sourceMappingURL=css.mjs.map\n"],
|
||||
"mappings": ";;;;;;AACA,OAAO;;;ACIP,OAAO;",
|
||||
"names": []
|
||||
}
|
||||
|
||||
4
admin/node_modules/.vite/deps/element-plus_es_components_form-item_style_css.js
generated
vendored
4
admin/node_modules/.vite/deps/element-plus_es_components_form-item_style_css.js
generated
vendored
@ -1,5 +1,5 @@
|
||||
import "./chunk-WNNLDN6V.js";
|
||||
import "./chunk-IV6PSERC.js";
|
||||
|
||||
// node_modules/element-plus/es/components/form-item/style/css.mjs
|
||||
import "E:/workspace/yingsa/admin/node_modules/element-plus/theme-chalk/el-form-item.css";
|
||||
import "E:/workspace/yingsha/admin/node_modules/element-plus/theme-chalk/el-form-item.css";
|
||||
//# sourceMappingURL=element-plus_es_components_form-item_style_css.js.map
|
||||
|
||||
2
admin/node_modules/.vite/deps/element-plus_es_components_form-item_style_css.js.map
generated
vendored
2
admin/node_modules/.vite/deps/element-plus_es_components_form-item_style_css.js.map
generated
vendored
@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../element-plus/es/components/form-item/style/css.mjs"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\r\nimport 'element-plus/theme-chalk/el-form-item.css';\r\n//# sourceMappingURL=css.mjs.map\r\n"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\nimport 'element-plus/theme-chalk/el-form-item.css';\n//# sourceMappingURL=css.mjs.map\n"],
|
||||
"mappings": ";;;AACA,OAAO;",
|
||||
"names": []
|
||||
}
|
||||
|
||||
4
admin/node_modules/.vite/deps/element-plus_es_components_form_style_css.js
generated
vendored
4
admin/node_modules/.vite/deps/element-plus_es_components_form_style_css.js
generated
vendored
@ -1,5 +1,5 @@
|
||||
import "./chunk-WNNLDN6V.js";
|
||||
import "./chunk-IV6PSERC.js";
|
||||
|
||||
// node_modules/element-plus/es/components/form/style/css.mjs
|
||||
import "E:/workspace/yingsa/admin/node_modules/element-plus/theme-chalk/el-form.css";
|
||||
import "E:/workspace/yingsha/admin/node_modules/element-plus/theme-chalk/el-form.css";
|
||||
//# sourceMappingURL=element-plus_es_components_form_style_css.js.map
|
||||
|
||||
2
admin/node_modules/.vite/deps/element-plus_es_components_form_style_css.js.map
generated
vendored
2
admin/node_modules/.vite/deps/element-plus_es_components_form_style_css.js.map
generated
vendored
@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../element-plus/es/components/form/style/css.mjs"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\r\nimport 'element-plus/theme-chalk/el-form.css';\r\n//# sourceMappingURL=css.mjs.map\r\n"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\nimport 'element-plus/theme-chalk/el-form.css';\n//# sourceMappingURL=css.mjs.map\n"],
|
||||
"mappings": ";;;AACA,OAAO;",
|
||||
"names": []
|
||||
}
|
||||
|
||||
4
admin/node_modules/.vite/deps/element-plus_es_components_header_style_css.js
generated
vendored
4
admin/node_modules/.vite/deps/element-plus_es_components_header_style_css.js
generated
vendored
@ -1,5 +1,5 @@
|
||||
import "./chunk-WNNLDN6V.js";
|
||||
import "./chunk-IV6PSERC.js";
|
||||
|
||||
// node_modules/element-plus/es/components/header/style/css.mjs
|
||||
import "E:/workspace/yingsa/admin/node_modules/element-plus/theme-chalk/el-header.css";
|
||||
import "E:/workspace/yingsha/admin/node_modules/element-plus/theme-chalk/el-header.css";
|
||||
//# sourceMappingURL=element-plus_es_components_header_style_css.js.map
|
||||
|
||||
2
admin/node_modules/.vite/deps/element-plus_es_components_header_style_css.js.map
generated
vendored
2
admin/node_modules/.vite/deps/element-plus_es_components_header_style_css.js.map
generated
vendored
@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../element-plus/es/components/header/style/css.mjs"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\r\nimport 'element-plus/theme-chalk/el-header.css';\r\n//# sourceMappingURL=css.mjs.map\r\n"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\nimport 'element-plus/theme-chalk/el-header.css';\n//# sourceMappingURL=css.mjs.map\n"],
|
||||
"mappings": ";;;AACA,OAAO;",
|
||||
"names": []
|
||||
}
|
||||
|
||||
2
admin/node_modules/.vite/deps/element-plus_es_components_icon_style_css.js
generated
vendored
2
admin/node_modules/.vite/deps/element-plus_es_components_icon_style_css.js
generated
vendored
@ -1,2 +1,2 @@
|
||||
import "./chunk-WNNLDN6V.js";
|
||||
import "./chunk-IV6PSERC.js";
|
||||
//# sourceMappingURL=element-plus_es_components_icon_style_css.js.map
|
||||
|
||||
6
admin/node_modules/.vite/deps/element-plus_es_components_input-number_style_css.js
generated
vendored
6
admin/node_modules/.vite/deps/element-plus_es_components_input-number_style_css.js
generated
vendored
@ -1,6 +1,6 @@
|
||||
import "./chunk-TBYZ47XG.js";
|
||||
import "./chunk-WNNLDN6V.js";
|
||||
import "./chunk-NKQWFVTF.js";
|
||||
import "./chunk-IV6PSERC.js";
|
||||
|
||||
// node_modules/element-plus/es/components/input-number/style/css.mjs
|
||||
import "E:/workspace/yingsa/admin/node_modules/element-plus/theme-chalk/el-input-number.css";
|
||||
import "E:/workspace/yingsha/admin/node_modules/element-plus/theme-chalk/el-input-number.css";
|
||||
//# sourceMappingURL=element-plus_es_components_input-number_style_css.js.map
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../element-plus/es/components/input-number/style/css.mjs"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\r\nimport '../../input/style/css.mjs';\r\nimport 'element-plus/theme-chalk/el-input-number.css';\r\n//# sourceMappingURL=css.mjs.map\r\n"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\nimport '../../input/style/css.mjs';\nimport 'element-plus/theme-chalk/el-input-number.css';\n//# sourceMappingURL=css.mjs.map\n"],
|
||||
"mappings": ";;;;AAEA,OAAO;",
|
||||
"names": []
|
||||
}
|
||||
|
||||
4
admin/node_modules/.vite/deps/element-plus_es_components_input_style_css.js
generated
vendored
4
admin/node_modules/.vite/deps/element-plus_es_components_input_style_css.js
generated
vendored
@ -1,3 +1,3 @@
|
||||
import "./chunk-TBYZ47XG.js";
|
||||
import "./chunk-WNNLDN6V.js";
|
||||
import "./chunk-NKQWFVTF.js";
|
||||
import "./chunk-IV6PSERC.js";
|
||||
//# sourceMappingURL=element-plus_es_components_input_style_css.js.map
|
||||
|
||||
4
admin/node_modules/.vite/deps/element-plus_es_components_loading_style_css.js
generated
vendored
4
admin/node_modules/.vite/deps/element-plus_es_components_loading_style_css.js
generated
vendored
@ -1,5 +1,5 @@
|
||||
import "./chunk-WNNLDN6V.js";
|
||||
import "./chunk-IV6PSERC.js";
|
||||
|
||||
// node_modules/element-plus/es/components/loading/style/css.mjs
|
||||
import "E:/workspace/yingsa/admin/node_modules/element-plus/theme-chalk/el-loading.css";
|
||||
import "E:/workspace/yingsha/admin/node_modules/element-plus/theme-chalk/el-loading.css";
|
||||
//# sourceMappingURL=element-plus_es_components_loading_style_css.js.map
|
||||
|
||||
2
admin/node_modules/.vite/deps/element-plus_es_components_loading_style_css.js.map
generated
vendored
2
admin/node_modules/.vite/deps/element-plus_es_components_loading_style_css.js.map
generated
vendored
@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../element-plus/es/components/loading/style/css.mjs"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\r\nimport 'element-plus/theme-chalk/el-loading.css';\r\n//# sourceMappingURL=css.mjs.map\r\n"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\nimport 'element-plus/theme-chalk/el-loading.css';\n//# sourceMappingURL=css.mjs.map\n"],
|
||||
"mappings": ";;;AACA,OAAO;",
|
||||
"names": []
|
||||
}
|
||||
|
||||
4
admin/node_modules/.vite/deps/element-plus_es_components_main_style_css.js
generated
vendored
4
admin/node_modules/.vite/deps/element-plus_es_components_main_style_css.js
generated
vendored
@ -1,5 +1,5 @@
|
||||
import "./chunk-WNNLDN6V.js";
|
||||
import "./chunk-IV6PSERC.js";
|
||||
|
||||
// node_modules/element-plus/es/components/main/style/css.mjs
|
||||
import "E:/workspace/yingsa/admin/node_modules/element-plus/theme-chalk/el-main.css";
|
||||
import "E:/workspace/yingsha/admin/node_modules/element-plus/theme-chalk/el-main.css";
|
||||
//# sourceMappingURL=element-plus_es_components_main_style_css.js.map
|
||||
|
||||
2
admin/node_modules/.vite/deps/element-plus_es_components_main_style_css.js.map
generated
vendored
2
admin/node_modules/.vite/deps/element-plus_es_components_main_style_css.js.map
generated
vendored
@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../element-plus/es/components/main/style/css.mjs"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\r\nimport 'element-plus/theme-chalk/el-main.css';\r\n//# sourceMappingURL=css.mjs.map\r\n"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\nimport 'element-plus/theme-chalk/el-main.css';\n//# sourceMappingURL=css.mjs.map\n"],
|
||||
"mappings": ";;;AACA,OAAO;",
|
||||
"names": []
|
||||
}
|
||||
|
||||
4
admin/node_modules/.vite/deps/element-plus_es_components_menu-item_style_css.js
generated
vendored
4
admin/node_modules/.vite/deps/element-plus_es_components_menu-item_style_css.js
generated
vendored
@ -1,5 +1,5 @@
|
||||
import "./chunk-WNNLDN6V.js";
|
||||
import "./chunk-IV6PSERC.js";
|
||||
|
||||
// node_modules/element-plus/es/components/menu-item/style/css.mjs
|
||||
import "E:/workspace/yingsa/admin/node_modules/element-plus/theme-chalk/el-menu-item.css";
|
||||
import "E:/workspace/yingsha/admin/node_modules/element-plus/theme-chalk/el-menu-item.css";
|
||||
//# sourceMappingURL=element-plus_es_components_menu-item_style_css.js.map
|
||||
|
||||
2
admin/node_modules/.vite/deps/element-plus_es_components_menu-item_style_css.js.map
generated
vendored
2
admin/node_modules/.vite/deps/element-plus_es_components_menu-item_style_css.js.map
generated
vendored
@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../element-plus/es/components/menu-item/style/css.mjs"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\r\nimport 'element-plus/theme-chalk/el-menu-item.css';\r\n//# sourceMappingURL=css.mjs.map\r\n"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\nimport 'element-plus/theme-chalk/el-menu-item.css';\n//# sourceMappingURL=css.mjs.map\n"],
|
||||
"mappings": ";;;AACA,OAAO;",
|
||||
"names": []
|
||||
}
|
||||
|
||||
8
admin/node_modules/.vite/deps/element-plus_es_components_menu_style_css.js
generated
vendored
8
admin/node_modules/.vite/deps/element-plus_es_components_menu_style_css.js
generated
vendored
@ -1,7 +1,7 @@
|
||||
import "./chunk-ZCVI6XFC.js";
|
||||
import "./chunk-2XFUNMCG.js";
|
||||
import "./chunk-WNNLDN6V.js";
|
||||
import "./chunk-R5DNQ3QC.js";
|
||||
import "./chunk-TX5YLZ4O.js";
|
||||
import "./chunk-IV6PSERC.js";
|
||||
|
||||
// node_modules/element-plus/es/components/menu/style/css.mjs
|
||||
import "E:/workspace/yingsa/admin/node_modules/element-plus/theme-chalk/el-menu.css";
|
||||
import "E:/workspace/yingsha/admin/node_modules/element-plus/theme-chalk/el-menu.css";
|
||||
//# sourceMappingURL=element-plus_es_components_menu_style_css.js.map
|
||||
|
||||
2
admin/node_modules/.vite/deps/element-plus_es_components_menu_style_css.js.map
generated
vendored
2
admin/node_modules/.vite/deps/element-plus_es_components_menu_style_css.js.map
generated
vendored
@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../element-plus/es/components/menu/style/css.mjs"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\r\nimport 'element-plus/theme-chalk/el-menu.css';\r\nimport '../../tooltip/style/css.mjs';\r\n//# sourceMappingURL=css.mjs.map\r\n"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\nimport 'element-plus/theme-chalk/el-menu.css';\nimport '../../tooltip/style/css.mjs';\n//# sourceMappingURL=css.mjs.map\n"],
|
||||
"mappings": ";;;;;AACA,OAAO;",
|
||||
"names": []
|
||||
}
|
||||
|
||||
4
admin/node_modules/.vite/deps/element-plus_es_components_option_style_css.js
generated
vendored
4
admin/node_modules/.vite/deps/element-plus_es_components_option_style_css.js
generated
vendored
@ -1,3 +1,3 @@
|
||||
import "./chunk-SB5RV2IU.js";
|
||||
import "./chunk-WNNLDN6V.js";
|
||||
import "./chunk-UBLR4G7Q.js";
|
||||
import "./chunk-IV6PSERC.js";
|
||||
//# sourceMappingURL=element-plus_es_components_option_style_css.js.map
|
||||
|
||||
16
admin/node_modules/.vite/deps/element-plus_es_components_pagination_style_css.js
generated
vendored
16
admin/node_modules/.vite/deps/element-plus_es_components_pagination_style_css.js
generated
vendored
@ -1,11 +1,11 @@
|
||||
import "./chunk-LZQ3XESL.js";
|
||||
import "./chunk-SB5RV2IU.js";
|
||||
import "./chunk-P5E57UKJ.js";
|
||||
import "./chunk-MGVC5NZO.js";
|
||||
import "./chunk-2XFUNMCG.js";
|
||||
import "./chunk-TBYZ47XG.js";
|
||||
import "./chunk-WNNLDN6V.js";
|
||||
import "./chunk-75C4BP7B.js";
|
||||
import "./chunk-UBLR4G7Q.js";
|
||||
import "./chunk-5KK3TTMN.js";
|
||||
import "./chunk-NKQWFVTF.js";
|
||||
import "./chunk-REWOA3VH.js";
|
||||
import "./chunk-TX5YLZ4O.js";
|
||||
import "./chunk-IV6PSERC.js";
|
||||
|
||||
// node_modules/element-plus/es/components/pagination/style/css.mjs
|
||||
import "E:/workspace/yingsa/admin/node_modules/element-plus/theme-chalk/el-pagination.css";
|
||||
import "E:/workspace/yingsha/admin/node_modules/element-plus/theme-chalk/el-pagination.css";
|
||||
//# sourceMappingURL=element-plus_es_components_pagination_style_css.js.map
|
||||
|
||||
2
admin/node_modules/.vite/deps/element-plus_es_components_pagination_style_css.js.map
generated
vendored
2
admin/node_modules/.vite/deps/element-plus_es_components_pagination_style_css.js.map
generated
vendored
@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../element-plus/es/components/pagination/style/css.mjs"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\r\nimport 'element-plus/theme-chalk/el-pagination.css';\r\nimport '../../select/style/css.mjs';\r\nimport '../../input/style/css.mjs';\r\n//# sourceMappingURL=css.mjs.map\r\n"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\nimport 'element-plus/theme-chalk/el-pagination.css';\nimport '../../select/style/css.mjs';\nimport '../../input/style/css.mjs';\n//# sourceMappingURL=css.mjs.map\n"],
|
||||
"mappings": ";;;;;;;;;AACA,OAAO;",
|
||||
"names": []
|
||||
}
|
||||
|
||||
6
admin/node_modules/.vite/deps/element-plus_es_components_radio-group_style_css.js
generated
vendored
6
admin/node_modules/.vite/deps/element-plus_es_components_radio-group_style_css.js
generated
vendored
@ -1,6 +1,6 @@
|
||||
import "./chunk-JJYIKYUB.js";
|
||||
import "./chunk-WNNLDN6V.js";
|
||||
import "./chunk-4PW274X2.js";
|
||||
import "./chunk-IV6PSERC.js";
|
||||
|
||||
// node_modules/element-plus/es/components/radio-group/style/css.mjs
|
||||
import "E:/workspace/yingsa/admin/node_modules/element-plus/theme-chalk/el-radio-group.css";
|
||||
import "E:/workspace/yingsha/admin/node_modules/element-plus/theme-chalk/el-radio-group.css";
|
||||
//# sourceMappingURL=element-plus_es_components_radio-group_style_css.js.map
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../element-plus/es/components/radio-group/style/css.mjs"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\r\nimport '../../radio/style/css.mjs';\r\nimport 'element-plus/theme-chalk/el-radio-group.css';\r\n//# sourceMappingURL=css.mjs.map\r\n"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\nimport '../../radio/style/css.mjs';\nimport 'element-plus/theme-chalk/el-radio-group.css';\n//# sourceMappingURL=css.mjs.map\n"],
|
||||
"mappings": ";;;;AAEA,OAAO;",
|
||||
"names": []
|
||||
}
|
||||
|
||||
4
admin/node_modules/.vite/deps/element-plus_es_components_radio_style_css.js
generated
vendored
4
admin/node_modules/.vite/deps/element-plus_es_components_radio_style_css.js
generated
vendored
@ -1,3 +1,3 @@
|
||||
import "./chunk-JJYIKYUB.js";
|
||||
import "./chunk-WNNLDN6V.js";
|
||||
import "./chunk-4PW274X2.js";
|
||||
import "./chunk-IV6PSERC.js";
|
||||
//# sourceMappingURL=element-plus_es_components_radio_style_css.js.map
|
||||
|
||||
4
admin/node_modules/.vite/deps/element-plus_es_components_row_style_css.js
generated
vendored
4
admin/node_modules/.vite/deps/element-plus_es_components_row_style_css.js
generated
vendored
@ -1,5 +1,5 @@
|
||||
import "./chunk-WNNLDN6V.js";
|
||||
import "./chunk-IV6PSERC.js";
|
||||
|
||||
// node_modules/element-plus/es/components/row/style/css.mjs
|
||||
import "E:/workspace/yingsa/admin/node_modules/element-plus/theme-chalk/el-row.css";
|
||||
import "E:/workspace/yingsha/admin/node_modules/element-plus/theme-chalk/el-row.css";
|
||||
//# sourceMappingURL=element-plus_es_components_row_style_css.js.map
|
||||
|
||||
2
admin/node_modules/.vite/deps/element-plus_es_components_row_style_css.js.map
generated
vendored
2
admin/node_modules/.vite/deps/element-plus_es_components_row_style_css.js.map
generated
vendored
@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../element-plus/es/components/row/style/css.mjs"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\r\nimport 'element-plus/theme-chalk/el-row.css';\r\n//# sourceMappingURL=css.mjs.map\r\n"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\nimport 'element-plus/theme-chalk/el-row.css';\n//# sourceMappingURL=css.mjs.map\n"],
|
||||
"mappings": ";;;AACA,OAAO;",
|
||||
"names": []
|
||||
}
|
||||
|
||||
12
admin/node_modules/.vite/deps/element-plus_es_components_select_style_css.js
generated
vendored
12
admin/node_modules/.vite/deps/element-plus_es_components_select_style_css.js
generated
vendored
@ -1,7 +1,7 @@
|
||||
import "./chunk-LZQ3XESL.js";
|
||||
import "./chunk-SB5RV2IU.js";
|
||||
import "./chunk-P5E57UKJ.js";
|
||||
import "./chunk-MGVC5NZO.js";
|
||||
import "./chunk-2XFUNMCG.js";
|
||||
import "./chunk-WNNLDN6V.js";
|
||||
import "./chunk-75C4BP7B.js";
|
||||
import "./chunk-UBLR4G7Q.js";
|
||||
import "./chunk-5KK3TTMN.js";
|
||||
import "./chunk-REWOA3VH.js";
|
||||
import "./chunk-TX5YLZ4O.js";
|
||||
import "./chunk-IV6PSERC.js";
|
||||
//# sourceMappingURL=element-plus_es_components_select_style_css.js.map
|
||||
|
||||
8
admin/node_modules/.vite/deps/element-plus_es_components_table-column_style_css.js
generated
vendored
8
admin/node_modules/.vite/deps/element-plus_es_components_table-column_style_css.js
generated
vendored
@ -1,7 +1,7 @@
|
||||
import "./chunk-P5E57UKJ.js";
|
||||
import "./chunk-3Y6T2BWZ.js";
|
||||
import "./chunk-WNNLDN6V.js";
|
||||
import "./chunk-B2YDYSZR.js";
|
||||
import "./chunk-5KK3TTMN.js";
|
||||
import "./chunk-IV6PSERC.js";
|
||||
|
||||
// node_modules/element-plus/es/components/table-column/style/css.mjs
|
||||
import "E:/workspace/yingsa/admin/node_modules/element-plus/theme-chalk/el-table-column.css";
|
||||
import "E:/workspace/yingsha/admin/node_modules/element-plus/theme-chalk/el-table-column.css";
|
||||
//# sourceMappingURL=element-plus_es_components_table-column_style_css.js.map
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../element-plus/es/components/table-column/style/css.mjs"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\r\nimport 'element-plus/theme-chalk/el-table-column.css';\r\nimport '../../checkbox/style/css.mjs';\r\nimport '../../tag/style/css.mjs';\r\n//# sourceMappingURL=css.mjs.map\r\n"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\nimport 'element-plus/theme-chalk/el-table-column.css';\nimport '../../checkbox/style/css.mjs';\nimport '../../tag/style/css.mjs';\n//# sourceMappingURL=css.mjs.map\n"],
|
||||
"mappings": ";;;;;AACA,OAAO;",
|
||||
"names": []
|
||||
}
|
||||
|
||||
12
admin/node_modules/.vite/deps/element-plus_es_components_table_style_css.js
generated
vendored
12
admin/node_modules/.vite/deps/element-plus_es_components_table_style_css.js
generated
vendored
@ -1,9 +1,9 @@
|
||||
import "./chunk-3Y6T2BWZ.js";
|
||||
import "./chunk-ZCVI6XFC.js";
|
||||
import "./chunk-MGVC5NZO.js";
|
||||
import "./chunk-2XFUNMCG.js";
|
||||
import "./chunk-WNNLDN6V.js";
|
||||
import "./chunk-B2YDYSZR.js";
|
||||
import "./chunk-R5DNQ3QC.js";
|
||||
import "./chunk-REWOA3VH.js";
|
||||
import "./chunk-TX5YLZ4O.js";
|
||||
import "./chunk-IV6PSERC.js";
|
||||
|
||||
// node_modules/element-plus/es/components/table/style/css.mjs
|
||||
import "E:/workspace/yingsa/admin/node_modules/element-plus/theme-chalk/el-table.css";
|
||||
import "E:/workspace/yingsha/admin/node_modules/element-plus/theme-chalk/el-table.css";
|
||||
//# sourceMappingURL=element-plus_es_components_table_style_css.js.map
|
||||
|
||||
2
admin/node_modules/.vite/deps/element-plus_es_components_table_style_css.js.map
generated
vendored
2
admin/node_modules/.vite/deps/element-plus_es_components_table_style_css.js.map
generated
vendored
@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../element-plus/es/components/table/style/css.mjs"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\r\nimport 'element-plus/theme-chalk/el-table.css';\r\nimport '../../checkbox/style/css.mjs';\r\nimport '../../tooltip/style/css.mjs';\r\nimport '../../scrollbar/style/css.mjs';\r\n//# sourceMappingURL=css.mjs.map\r\n"],
|
||||
"sourcesContent": ["import '../../base/style/css.mjs';\nimport 'element-plus/theme-chalk/el-table.css';\nimport '../../checkbox/style/css.mjs';\nimport '../../tooltip/style/css.mjs';\nimport '../../scrollbar/style/css.mjs';\n//# sourceMappingURL=css.mjs.map\n"],
|
||||
"mappings": ";;;;;;;AACA,OAAO;",
|
||||
"names": []
|
||||
}
|
||||
|
||||
4
admin/node_modules/.vite/deps/element-plus_es_components_tag_style_css.js
generated
vendored
4
admin/node_modules/.vite/deps/element-plus_es_components_tag_style_css.js
generated
vendored
@ -1,3 +1,3 @@
|
||||
import "./chunk-P5E57UKJ.js";
|
||||
import "./chunk-WNNLDN6V.js";
|
||||
import "./chunk-5KK3TTMN.js";
|
||||
import "./chunk-IV6PSERC.js";
|
||||
//# sourceMappingURL=element-plus_es_components_tag_style_css.js.map
|
||||
|
||||
6
admin/node_modules/.vite/deps/pinia.js
generated
vendored
6
admin/node_modules/.vite/deps/pinia.js
generated
vendored
@ -1,11 +1,11 @@
|
||||
import {
|
||||
setupDevtoolsPlugin
|
||||
} from "./chunk-VID4RN2V.js";
|
||||
} from "./chunk-YFT6OQ5R.js";
|
||||
import {
|
||||
del,
|
||||
isVue2,
|
||||
set
|
||||
} from "./chunk-YAGW2SQC.js";
|
||||
} from "./chunk-HYZ2CRGS.js";
|
||||
import {
|
||||
computed,
|
||||
effectScope,
|
||||
@ -25,7 +25,7 @@ import {
|
||||
toRefs,
|
||||
unref,
|
||||
watch
|
||||
} from "./chunk-ELEEJBJQ.js";
|
||||
} from "./chunk-H2732BJL.js";
|
||||
import "./chunk-G3PMV62Z.js";
|
||||
|
||||
// node_modules/pinia/dist/pinia.mjs
|
||||
|
||||
2
admin/node_modules/.vite/deps/pinia.js.map
generated
vendored
2
admin/node_modules/.vite/deps/pinia.js.map
generated
vendored
File diff suppressed because one or more lines are too long
2
admin/node_modules/.vite/deps/qrcode.js.map
generated
vendored
2
admin/node_modules/.vite/deps/qrcode.js.map
generated
vendored
File diff suppressed because one or more lines are too long
4
admin/node_modules/.vite/deps/vue-router.js
generated
vendored
4
admin/node_modules/.vite/deps/vue-router.js
generated
vendored
@ -1,6 +1,6 @@
|
||||
import {
|
||||
setupDevtoolsPlugin
|
||||
} from "./chunk-VID4RN2V.js";
|
||||
} from "./chunk-YFT6OQ5R.js";
|
||||
import {
|
||||
computed,
|
||||
defineComponent,
|
||||
@ -19,7 +19,7 @@ import {
|
||||
unref,
|
||||
watch,
|
||||
watchEffect
|
||||
} from "./chunk-ELEEJBJQ.js";
|
||||
} from "./chunk-H2732BJL.js";
|
||||
import "./chunk-G3PMV62Z.js";
|
||||
|
||||
// node_modules/vue-router/dist/devtools-EWN81iOl.mjs
|
||||
|
||||
2
admin/node_modules/.vite/deps/vue-router.js.map
generated
vendored
2
admin/node_modules/.vite/deps/vue-router.js.map
generated
vendored
File diff suppressed because one or more lines are too long
2
admin/node_modules/.vite/deps/vue.js
generated
vendored
2
admin/node_modules/.vite/deps/vue.js
generated
vendored
@ -170,7 +170,7 @@ import {
|
||||
withMemo,
|
||||
withModifiers,
|
||||
withScopeId
|
||||
} from "./chunk-ELEEJBJQ.js";
|
||||
} from "./chunk-H2732BJL.js";
|
||||
import "./chunk-G3PMV62Z.js";
|
||||
export {
|
||||
BaseTransition,
|
||||
|
||||
@ -5,3 +5,6 @@ export const getPublicStores = () => request.get('/store/list')
|
||||
|
||||
// 获取公开天梯排名
|
||||
export const getPublicRanking = (params) => request.get('/ladder/ranking', { params })
|
||||
|
||||
// 大屏:进行中/近7天比赛列表
|
||||
export const getDisplayMatches = (params) => request.get('/match/display-list', { params })
|
||||
|
||||
@ -87,6 +87,18 @@ const routes = [
|
||||
component: () => import("@/views/display/RankingBoardOrange.vue"),
|
||||
meta: { title: "天梯排行大屏(橙色)", public: true },
|
||||
},
|
||||
{
|
||||
path: "/display/ladder-summary",
|
||||
name: "LadderSummary",
|
||||
component: () => import("@/views/display/LadderSummary.vue"),
|
||||
meta: { title: "天梯汇总大屏", public: true },
|
||||
},
|
||||
{
|
||||
path: "/display/ladder-summary-orange",
|
||||
name: "LadderSummaryOrange",
|
||||
component: () => import("@/views/display/LadderSummaryOrange.vue"),
|
||||
meta: { title: "天梯汇总大屏(橙色)", public: true },
|
||||
},
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
|
||||
@ -59,6 +59,10 @@
|
||||
<el-icon class="icon"><Monitor /></el-icon>
|
||||
<span class="label">天梯大屏(橙)</span>
|
||||
</div>
|
||||
<div class="quick-action-btn" @click="$router.push('/display/ladder-summary')">
|
||||
<el-icon class="icon"><Monitor /></el-icon>
|
||||
<span class="label">天梯汇总大屏</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
885
admin/src/views/display/LadderSummary.vue
Normal file
885
admin/src/views/display/LadderSummary.vue
Normal file
@ -0,0 +1,885 @@
|
||||
<template>
|
||||
<div class="ladder-summary-container">
|
||||
<div class="top-bar">
|
||||
<div class="time-box">{{ currentTime }}</div>
|
||||
<div class="title-box">
|
||||
<h1 class="main-title">英飒俱乐部 · 天梯汇总榜</h1>
|
||||
<div class="sub-title">男女 · 段位 · 前15名</div>
|
||||
</div>
|
||||
<div class="right-controls">
|
||||
<div
|
||||
class="style-switcher"
|
||||
@click="router.push({ path: '/display/ladder-summary-orange', query: route.query })">
|
||||
<span>切换 Premium 风格</span>
|
||||
</div>
|
||||
<div class="store-box" @click.stop="showStoreMenu = !showStoreMenu">
|
||||
<span class="label">场馆:</span>
|
||||
<span class="value">{{ currentStore?.name || "未选择" }}</span>
|
||||
<el-icon :class="{ rotate: showStoreMenu }"><ArrowDown /></el-icon>
|
||||
<transition name="fade">
|
||||
<div v-if="showStoreMenu" class="store-dropdown" @click.stop>
|
||||
<div
|
||||
v-for="store in stores"
|
||||
:key="store.id"
|
||||
class="dropdown-item"
|
||||
:class="{ active: currentStore?.id === store.id }"
|
||||
@click="selectStore(store)">
|
||||
{{ store.name }}
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="boards">
|
||||
<div class="boards-header">
|
||||
<div class="boards-title">排行榜(前15名)</div>
|
||||
<div v-if="loadingBoards" class="boards-loading">加载中...</div>
|
||||
</div>
|
||||
|
||||
<div class="boards-grid" ref="boardsScrollEl">
|
||||
<div class="scroll-wrapper" :style="{ transform: `translateY(${boardsScrollY}px)` }">
|
||||
<div class="scroll-group" ref="boardsGroup1">
|
||||
<div v-for="level in levels" :key="`b1-${level.value}`" class="level-row">
|
||||
<div class="level-label">{{ level.label }}</div>
|
||||
<div class="gender-columns">
|
||||
<div class="board-card">
|
||||
<div class="board-header">
|
||||
<div class="board-name">男子榜</div>
|
||||
<div class="board-meta">Top 15</div>
|
||||
</div>
|
||||
<div class="board-list">
|
||||
<div
|
||||
v-for="item in getBoardList(1, level.value)"
|
||||
:key="`m-1-${level.value}-${item.id}`"
|
||||
class="board-item">
|
||||
<div class="rank">{{ item.rank }}</div>
|
||||
<div class="name">{{ item.realName || item.nickname || "-" }}</div>
|
||||
<div class="score">{{ item.powerScore }}</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!loadingBoards && getBoardList(1, level.value).length === 0"
|
||||
class="empty">
|
||||
暂无数据
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="board-card">
|
||||
<div class="board-header">
|
||||
<div class="board-name">女子榜</div>
|
||||
<div class="board-meta">Top 15</div>
|
||||
</div>
|
||||
<div class="board-list">
|
||||
<div
|
||||
v-for="item in getBoardList(2, level.value)"
|
||||
:key="`f-1-${level.value}-${item.id}`"
|
||||
class="board-item">
|
||||
<div class="rank">{{ item.rank }}</div>
|
||||
<div class="name">{{ item.realName || item.nickname || "-" }}</div>
|
||||
<div class="score">{{ item.powerScore }}</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!loadingBoards && getBoardList(2, level.value).length === 0"
|
||||
class="empty">
|
||||
暂无数据
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="scroll-group">
|
||||
<div v-for="level in levels" :key="`b2-${level.value}`" class="level-row">
|
||||
<div class="level-label">{{ level.label }}</div>
|
||||
<div class="gender-columns">
|
||||
<div class="board-card">
|
||||
<div class="board-header">
|
||||
<div class="board-name">男子榜</div>
|
||||
<div class="board-meta">Top 15</div>
|
||||
</div>
|
||||
<div class="board-list">
|
||||
<div
|
||||
v-for="item in getBoardList(1, level.value)"
|
||||
:key="`m-2-${level.value}-${item.id}`"
|
||||
class="board-item">
|
||||
<div class="rank">{{ item.rank }}</div>
|
||||
<div class="name">{{ item.realName || item.nickname || "-" }}</div>
|
||||
<div class="score">{{ item.powerScore }}</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!loadingBoards && getBoardList(1, level.value).length === 0"
|
||||
class="empty">
|
||||
暂无数据
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="board-card">
|
||||
<div class="board-header">
|
||||
<div class="board-name">女子榜</div>
|
||||
<div class="board-meta">Top 15</div>
|
||||
</div>
|
||||
<div class="board-list">
|
||||
<div
|
||||
v-for="item in getBoardList(2, level.value)"
|
||||
:key="`f-2-${level.value}-${item.id}`"
|
||||
class="board-item">
|
||||
<div class="rank">{{ item.rank }}</div>
|
||||
<div class="name">{{ item.realName || item.nickname || "-" }}</div>
|
||||
<div class="score">{{ item.powerScore }}</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!loadingBoards && getBoardList(2, level.value).length === 0"
|
||||
class="empty">
|
||||
暂无数据
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="matches">
|
||||
<div class="matches-header">
|
||||
<div class="matches-title">进行中 / 近7天比赛</div>
|
||||
<div v-if="loadingMatches" class="matches-loading">加载中...</div>
|
||||
</div>
|
||||
<div class="matches-list" ref="matchesScrollEl">
|
||||
<div class="scroll-wrapper" :style="{ transform: `translateY(${matchesScrollY}px)` }">
|
||||
<div class="scroll-group" ref="matchesGroup1">
|
||||
<div v-for="m in matches" :key="`m1-${m.id}`" class="match-item">
|
||||
<div class="match-top">
|
||||
<div class="match-name">{{ m.name || m.matchCode }}</div>
|
||||
<div class="match-badges">
|
||||
<span class="badge type">{{ m.typeName }}</span>
|
||||
<span class="badge status" :class="statusClass(m.status)">{{
|
||||
m.statusName
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="match-meta">
|
||||
<span class="store">{{ m.storeName || "-" }}</span>
|
||||
<span class="time">{{ formatTime(m.startTime || m.createdAt) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!loadingMatches && matches.length === 0" class="empty">
|
||||
暂无比赛
|
||||
</div>
|
||||
</div>
|
||||
<div class="scroll-group">
|
||||
<div v-for="m in matches" :key="`m2-${m.id}`" class="match-item">
|
||||
<div class="match-top">
|
||||
<div class="match-name">{{ m.name || m.matchCode }}</div>
|
||||
<div class="match-badges">
|
||||
<span class="badge type">{{ m.typeName }}</span>
|
||||
<span class="badge status" :class="statusClass(m.status)">{{
|
||||
m.statusName
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="match-meta">
|
||||
<span class="store">{{ m.storeName || "-" }}</span>
|
||||
<span class="time">{{ formatTime(m.startTime || m.createdAt) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<span>数据实时同步 · 每30秒刷新</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, nextTick, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import dayjs from "dayjs";
|
||||
import { ArrowDown } from "@element-plus/icons-vue";
|
||||
import { getPublicStores, getPublicRanking, getDisplayMatches } from "@/api/display";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const currentTime = ref(dayjs().format("YYYY-MM-DD HH:mm:ss"));
|
||||
const stores = ref([]);
|
||||
const currentStore = ref(null);
|
||||
const showStoreMenu = ref(false);
|
||||
|
||||
const levels = ref([
|
||||
{ value: 1, label: "Lv1 新锐" },
|
||||
{ value: 2, label: "Lv2 精锐" },
|
||||
{ value: 3, label: "Lv3 高手" },
|
||||
{ value: 4, label: "Lv4 大师" },
|
||||
{ value: 5, label: "Lv5 宗师" },
|
||||
]);
|
||||
|
||||
const boards = ref({});
|
||||
const matches = ref([]);
|
||||
const loadingBoards = ref(false);
|
||||
const loadingMatches = ref(false);
|
||||
|
||||
let timer = null;
|
||||
let refreshTimer = null;
|
||||
let handleWindowClick = null;
|
||||
let boardsRaf = null;
|
||||
let matchesRaf = null;
|
||||
|
||||
const boardsScrollEl = ref(null);
|
||||
const boardsGroup1 = ref(null);
|
||||
const matchesScrollEl = ref(null);
|
||||
const matchesGroup1 = ref(null);
|
||||
|
||||
const boardsScrollY = ref(0);
|
||||
const matchesScrollY = ref(0);
|
||||
|
||||
const updateTime = () => {
|
||||
currentTime.value = dayjs().format("YYYY-MM-DD HH:mm:ss");
|
||||
};
|
||||
|
||||
const formatTime = (t) => {
|
||||
if (!t) return "-";
|
||||
return dayjs(t).format("MM-DD HH:mm");
|
||||
};
|
||||
|
||||
const statusClass = (s) => {
|
||||
if (s === 1) return "ongoing";
|
||||
if (s === 0) return "pending";
|
||||
if (s === 2) return "finished";
|
||||
return "cancelled";
|
||||
};
|
||||
|
||||
const getBoardKey = (gender, level) => `${gender}_${level}`;
|
||||
|
||||
const getBoardList = (gender, level) => {
|
||||
const key = getBoardKey(gender, level);
|
||||
return boards.value[key] || [];
|
||||
};
|
||||
|
||||
const fetchStores = async () => {
|
||||
const res = await getPublicStores();
|
||||
stores.value = res.data?.list || [];
|
||||
|
||||
const urlStoreId = route.query.store_id;
|
||||
if (urlStoreId) {
|
||||
const found = stores.value.find((s) => s.id === parseInt(urlStoreId));
|
||||
if (found) currentStore.value = found;
|
||||
}
|
||||
|
||||
if (!currentStore.value && stores.value.length > 0) {
|
||||
currentStore.value = stores.value[0];
|
||||
}
|
||||
};
|
||||
|
||||
const fetchBoards = async () => {
|
||||
if (!currentStore.value) return;
|
||||
loadingBoards.value = true;
|
||||
try {
|
||||
const tasks = [];
|
||||
for (const level of levels.value) {
|
||||
tasks.push(
|
||||
getPublicRanking({
|
||||
store_id: currentStore.value.id,
|
||||
gender: 1,
|
||||
level: level.value,
|
||||
page: 1,
|
||||
pageSize: 15,
|
||||
is_display: 1,
|
||||
no_count: 1,
|
||||
}).then((res) => ({ gender: 1, level: level.value, list: res.data?.list || [] })),
|
||||
);
|
||||
tasks.push(
|
||||
getPublicRanking({
|
||||
store_id: currentStore.value.id,
|
||||
gender: 2,
|
||||
level: level.value,
|
||||
page: 1,
|
||||
pageSize: 15,
|
||||
is_display: 1,
|
||||
no_count: 1,
|
||||
}).then((res) => ({ gender: 2, level: level.value, list: res.data?.list || [] })),
|
||||
);
|
||||
}
|
||||
|
||||
const result = await Promise.all(tasks);
|
||||
const nextBoards = {};
|
||||
for (const r of result) {
|
||||
nextBoards[getBoardKey(r.gender, r.level)] = r.list;
|
||||
}
|
||||
boards.value = nextBoards;
|
||||
} finally {
|
||||
loadingBoards.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchMatches = async () => {
|
||||
if (!currentStore.value) return;
|
||||
loadingMatches.value = true;
|
||||
try {
|
||||
const res = await getDisplayMatches({
|
||||
store_id: currentStore.value.id,
|
||||
days: 7,
|
||||
limit: 30,
|
||||
});
|
||||
matches.value = res.data || [];
|
||||
} finally {
|
||||
loadingMatches.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const refreshAll = async () => {
|
||||
await Promise.all([fetchBoards(), fetchMatches()]);
|
||||
};
|
||||
|
||||
const stopAutoScroll = () => {
|
||||
if (boardsRaf) cancelAnimationFrame(boardsRaf);
|
||||
if (matchesRaf) cancelAnimationFrame(matchesRaf);
|
||||
boardsRaf = null;
|
||||
matchesRaf = null;
|
||||
};
|
||||
|
||||
const startBoardsScroll = () => {
|
||||
if (!boardsScrollEl.value || !boardsGroup1.value) return;
|
||||
if (loadingBoards.value) return;
|
||||
|
||||
const tick = () => {
|
||||
if (!boardsScrollEl.value || !boardsGroup1.value) {
|
||||
boardsRaf = requestAnimationFrame(tick);
|
||||
return;
|
||||
}
|
||||
if (loadingBoards.value) {
|
||||
boardsRaf = requestAnimationFrame(tick);
|
||||
return;
|
||||
}
|
||||
|
||||
const h = boardsGroup1.value.offsetHeight;
|
||||
const containerH = boardsScrollEl.value.offsetHeight;
|
||||
if (!h || h <= containerH) {
|
||||
boardsScrollY.value = 0;
|
||||
boardsRaf = requestAnimationFrame(tick);
|
||||
return;
|
||||
}
|
||||
|
||||
boardsScrollY.value -= 0.35;
|
||||
if (Math.abs(boardsScrollY.value) >= h) {
|
||||
boardsScrollY.value = 0;
|
||||
}
|
||||
boardsRaf = requestAnimationFrame(tick);
|
||||
};
|
||||
|
||||
if (boardsRaf) cancelAnimationFrame(boardsRaf);
|
||||
boardsRaf = requestAnimationFrame(tick);
|
||||
};
|
||||
|
||||
const startMatchesScroll = () => {
|
||||
if (!matchesScrollEl.value || !matchesGroup1.value) return;
|
||||
if (loadingMatches.value) return;
|
||||
|
||||
const tick = () => {
|
||||
if (!matchesScrollEl.value || !matchesGroup1.value) {
|
||||
matchesRaf = requestAnimationFrame(tick);
|
||||
return;
|
||||
}
|
||||
if (loadingMatches.value) {
|
||||
matchesRaf = requestAnimationFrame(tick);
|
||||
return;
|
||||
}
|
||||
|
||||
const h = matchesGroup1.value.offsetHeight;
|
||||
const containerH = matchesScrollEl.value.offsetHeight;
|
||||
if (!h || h <= containerH) {
|
||||
matchesScrollY.value = 0;
|
||||
matchesRaf = requestAnimationFrame(tick);
|
||||
return;
|
||||
}
|
||||
|
||||
matchesScrollY.value -= 0.4;
|
||||
if (Math.abs(matchesScrollY.value) >= h) {
|
||||
matchesScrollY.value = 0;
|
||||
}
|
||||
matchesRaf = requestAnimationFrame(tick);
|
||||
};
|
||||
|
||||
if (matchesRaf) cancelAnimationFrame(matchesRaf);
|
||||
matchesRaf = requestAnimationFrame(tick);
|
||||
};
|
||||
|
||||
const selectStore = async (store) => {
|
||||
currentStore.value = store;
|
||||
showStoreMenu.value = false;
|
||||
router.replace({ query: { ...route.query, store_id: store.id } });
|
||||
await nextTick();
|
||||
stopAutoScroll();
|
||||
boardsScrollY.value = 0;
|
||||
matchesScrollY.value = 0;
|
||||
refreshAll();
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchStores();
|
||||
await refreshAll();
|
||||
|
||||
timer = setInterval(updateTime, 1000);
|
||||
refreshTimer = setInterval(refreshAll, 30 * 1000);
|
||||
|
||||
handleWindowClick = () => {
|
||||
showStoreMenu.value = false;
|
||||
};
|
||||
window.addEventListener("click", handleWindowClick);
|
||||
|
||||
nextTick(() => {
|
||||
startBoardsScroll();
|
||||
startMatchesScroll();
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) clearInterval(timer);
|
||||
if (refreshTimer) clearInterval(refreshTimer);
|
||||
stopAutoScroll();
|
||||
if (handleWindowClick) window.removeEventListener("click", handleWindowClick);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => loadingBoards.value,
|
||||
(v) => {
|
||||
if (!v) {
|
||||
nextTick(() => startBoardsScroll());
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => loadingMatches.value,
|
||||
(v) => {
|
||||
if (!v) {
|
||||
nextTick(() => startMatchesScroll());
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.ladder-summary-container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: #050a18;
|
||||
color: #fff;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
height: 84px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 40px;
|
||||
background: linear-gradient(to bottom, rgba(10, 25, 51, 0.9), transparent);
|
||||
position: relative;
|
||||
z-index: 50;
|
||||
border-bottom: 1px solid rgba(0, 242, 255, 0.15);
|
||||
}
|
||||
|
||||
.time-box {
|
||||
min-width: 260px;
|
||||
color: #00f2ff;
|
||||
font-size: 20px;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.title-box {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 62%;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
margin: 0;
|
||||
font-size: 40px;
|
||||
letter-spacing: 6px;
|
||||
font-weight: 900;
|
||||
background: linear-gradient(to bottom, #fff, #a5d8ff);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
margin-top: 6px;
|
||||
color: rgba(165, 216, 255, 0.8);
|
||||
font-size: 14px;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.right-controls {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.style-switcher {
|
||||
height: 38px;
|
||||
padding: 0 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(0, 242, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: rgba(165, 216, 255, 0.95);
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.style-switcher:hover {
|
||||
background: rgba(0, 242, 255, 0.1);
|
||||
border-color: #00f2ff;
|
||||
color: #00f2ff;
|
||||
}
|
||||
|
||||
.store-box {
|
||||
background: rgba(0, 242, 255, 0.1);
|
||||
border: 1px solid rgba(0, 242, 255, 0.3);
|
||||
height: 38px;
|
||||
padding: 0 14px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.store-box .label {
|
||||
color: rgba(165, 216, 255, 0.85);
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.store-box .value {
|
||||
color: #00f2ff;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
max-width: 220px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.store-box :deep(.el-icon) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.store-box .rotate {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.store-dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
right: 0;
|
||||
min-width: 220px;
|
||||
background: #0a1933;
|
||||
border: 1px solid #00f2ff;
|
||||
border-radius: 4px;
|
||||
padding: 10px 0;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6);
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 10px 16px;
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
background: rgba(0, 242, 255, 0.1);
|
||||
color: #00f2ff;
|
||||
}
|
||||
|
||||
.dropdown-item.active {
|
||||
color: #00f2ff;
|
||||
background: rgba(0, 242, 255, 0.12);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 360px;
|
||||
gap: 18px;
|
||||
padding: 18px 22px 14px;
|
||||
}
|
||||
|
||||
.boards,
|
||||
.matches {
|
||||
min-height: 0;
|
||||
border: 1px solid rgba(0, 242, 255, 0.12);
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
overflow: hidden;
|
||||
box-shadow: 0 0 30px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.boards-header,
|
||||
.matches-header {
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
background: rgba(0, 242, 255, 0.08);
|
||||
border-bottom: 1px solid rgba(0, 242, 255, 0.12);
|
||||
}
|
||||
|
||||
.boards-title,
|
||||
.matches-title {
|
||||
font-weight: 900;
|
||||
letter-spacing: 2px;
|
||||
color: #00f2ff;
|
||||
}
|
||||
|
||||
.boards-loading,
|
||||
.matches-loading {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.boards-grid {
|
||||
height: calc(100% - 48px);
|
||||
overflow: hidden;
|
||||
padding: 12px 12px 18px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.level-row {
|
||||
display: grid;
|
||||
grid-template-columns: 120px 1fr;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.level-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid rgba(0, 242, 255, 0.12);
|
||||
border-radius: 10px;
|
||||
background: rgba(0, 242, 255, 0.04);
|
||||
color: rgba(165, 216, 255, 0.95);
|
||||
font-weight: 800;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.gender-columns {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.board-card {
|
||||
border: 1px solid rgba(0, 242, 255, 0.1);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
.board-header {
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 12px;
|
||||
background: rgba(0, 242, 255, 0.07);
|
||||
border-bottom: 1px solid rgba(0, 242, 255, 0.1);
|
||||
}
|
||||
|
||||
.board-name {
|
||||
font-weight: 900;
|
||||
color: #00f2ff;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.board-meta {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.board-list {
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
.board-item {
|
||||
display: grid;
|
||||
grid-template-columns: 46px 1fr 72px;
|
||||
gap: 10px;
|
||||
padding: 6px 12px;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.board-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.rank {
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
font-weight: 800;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.name {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.score {
|
||||
text-align: right;
|
||||
color: #00f2ff;
|
||||
font-weight: 900;
|
||||
font-family: "Arial Black";
|
||||
}
|
||||
|
||||
.matches-list {
|
||||
height: calc(100% - 48px);
|
||||
overflow: hidden;
|
||||
padding: 10px 12px 14px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.scroll-wrapper {
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.match-item {
|
||||
border: 1px solid rgba(255, 152, 0, 0.12);
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border-radius: 10px;
|
||||
padding: 10px 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.match-top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.match-name {
|
||||
font-weight: 900;
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
line-height: 1.2;
|
||||
max-width: 220px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.match-badges {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 12px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.badge.type {
|
||||
color: rgba(255, 204, 128, 0.95);
|
||||
border-color: rgba(255, 152, 0, 0.18);
|
||||
background: rgba(255, 152, 0, 0.06);
|
||||
}
|
||||
|
||||
.badge.status.ongoing {
|
||||
color: #00f2ff;
|
||||
border-color: rgba(0, 242, 255, 0.25);
|
||||
background: rgba(0, 242, 255, 0.08);
|
||||
}
|
||||
|
||||
.badge.status.pending {
|
||||
color: rgba(165, 216, 255, 0.95);
|
||||
border-color: rgba(165, 216, 255, 0.18);
|
||||
background: rgba(165, 216, 255, 0.06);
|
||||
}
|
||||
|
||||
.badge.status.finished {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
border-color: rgba(255, 255, 255, 0.12);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.badge.status.cancelled {
|
||||
color: rgba(255, 90, 90, 0.9);
|
||||
border-color: rgba(255, 90, 90, 0.18);
|
||||
background: rgba(255, 90, 90, 0.08);
|
||||
}
|
||||
|
||||
.match-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 12px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer {
|
||||
height: 38px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
border-top: 1px solid rgba(0, 242, 255, 0.1);
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
887
admin/src/views/display/LadderSummaryOrange.vue
Normal file
887
admin/src/views/display/LadderSummaryOrange.vue
Normal file
@ -0,0 +1,887 @@
|
||||
<template>
|
||||
<div class="ladder-summary-orange">
|
||||
<div class="top-bar">
|
||||
<div class="time-box">{{ currentTime }}</div>
|
||||
<div class="title-box">
|
||||
<h1 class="main-title">英飒俱乐部 · 天梯汇总榜</h1>
|
||||
<div class="sub-title">男女 · 段位 · 前15名</div>
|
||||
</div>
|
||||
<div class="right-controls">
|
||||
<div
|
||||
class="style-switcher"
|
||||
@click="router.push({ path: '/display/ladder-summary', query: route.query })">
|
||||
<span>切换经典风格</span>
|
||||
</div>
|
||||
<div class="store-box" @click.stop="showStoreMenu = !showStoreMenu">
|
||||
<span class="label">场馆:</span>
|
||||
<span class="value">{{ currentStore?.name || "未选择" }}</span>
|
||||
<el-icon :class="{ rotate: showStoreMenu }"><ArrowDown /></el-icon>
|
||||
<transition name="fade">
|
||||
<div v-if="showStoreMenu" class="store-dropdown" @click.stop>
|
||||
<div
|
||||
v-for="store in stores"
|
||||
:key="store.id"
|
||||
class="dropdown-item"
|
||||
:class="{ active: currentStore?.id === store.id }"
|
||||
@click="selectStore(store)">
|
||||
{{ store.name }}
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="boards">
|
||||
<div class="boards-header">
|
||||
<div class="boards-title">排行榜(前15名)</div>
|
||||
<div v-if="loadingBoards" class="boards-loading">加载中...</div>
|
||||
</div>
|
||||
|
||||
<div class="boards-grid" ref="boardsScrollEl">
|
||||
<div class="scroll-wrapper" :style="{ transform: `translateY(${boardsScrollY}px)` }">
|
||||
<div class="scroll-group" ref="boardsGroup1">
|
||||
<div v-for="level in levels" :key="`b1-${level.value}`" class="level-row">
|
||||
<div class="level-label">{{ level.label }}</div>
|
||||
<div class="gender-columns">
|
||||
<div class="board-card">
|
||||
<div class="board-header">
|
||||
<div class="board-name">男子榜</div>
|
||||
<div class="board-meta">Top 15</div>
|
||||
</div>
|
||||
<div class="board-list">
|
||||
<div
|
||||
v-for="item in getBoardList(1, level.value)"
|
||||
:key="`m-1-${level.value}-${item.id}`"
|
||||
class="board-item">
|
||||
<div class="rank">{{ item.rank }}</div>
|
||||
<div class="name">{{ item.realName || item.nickname || "-" }}</div>
|
||||
<div class="score">{{ item.powerScore }}</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!loadingBoards && getBoardList(1, level.value).length === 0"
|
||||
class="empty">
|
||||
暂无数据
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="board-card">
|
||||
<div class="board-header">
|
||||
<div class="board-name">女子榜</div>
|
||||
<div class="board-meta">Top 15</div>
|
||||
</div>
|
||||
<div class="board-list">
|
||||
<div
|
||||
v-for="item in getBoardList(2, level.value)"
|
||||
:key="`f-1-${level.value}-${item.id}`"
|
||||
class="board-item">
|
||||
<div class="rank">{{ item.rank }}</div>
|
||||
<div class="name">{{ item.realName || item.nickname || "-" }}</div>
|
||||
<div class="score">{{ item.powerScore }}</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!loadingBoards && getBoardList(2, level.value).length === 0"
|
||||
class="empty">
|
||||
暂无数据
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="scroll-group">
|
||||
<div v-for="level in levels" :key="`b2-${level.value}`" class="level-row">
|
||||
<div class="level-label">{{ level.label }}</div>
|
||||
<div class="gender-columns">
|
||||
<div class="board-card">
|
||||
<div class="board-header">
|
||||
<div class="board-name">男子榜</div>
|
||||
<div class="board-meta">Top 15</div>
|
||||
</div>
|
||||
<div class="board-list">
|
||||
<div
|
||||
v-for="item in getBoardList(1, level.value)"
|
||||
:key="`m-2-${level.value}-${item.id}`"
|
||||
class="board-item">
|
||||
<div class="rank">{{ item.rank }}</div>
|
||||
<div class="name">{{ item.realName || item.nickname || "-" }}</div>
|
||||
<div class="score">{{ item.powerScore }}</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!loadingBoards && getBoardList(1, level.value).length === 0"
|
||||
class="empty">
|
||||
暂无数据
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="board-card">
|
||||
<div class="board-header">
|
||||
<div class="board-name">女子榜</div>
|
||||
<div class="board-meta">Top 15</div>
|
||||
</div>
|
||||
<div class="board-list">
|
||||
<div
|
||||
v-for="item in getBoardList(2, level.value)"
|
||||
:key="`f-2-${level.value}-${item.id}`"
|
||||
class="board-item">
|
||||
<div class="rank">{{ item.rank }}</div>
|
||||
<div class="name">{{ item.realName || item.nickname || "-" }}</div>
|
||||
<div class="score">{{ item.powerScore }}</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!loadingBoards && getBoardList(2, level.value).length === 0"
|
||||
class="empty">
|
||||
暂无数据
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="matches">
|
||||
<div class="matches-header">
|
||||
<div class="matches-title">进行中 / 近7天比赛</div>
|
||||
<div v-if="loadingMatches" class="matches-loading">加载中...</div>
|
||||
</div>
|
||||
<div class="matches-list" ref="matchesScrollEl">
|
||||
<div class="scroll-wrapper" :style="{ transform: `translateY(${matchesScrollY}px)` }">
|
||||
<div class="scroll-group" ref="matchesGroup1">
|
||||
<div v-for="m in matches" :key="`m1-${m.id}`" class="match-item">
|
||||
<div class="match-top">
|
||||
<div class="match-name">{{ m.name || m.matchCode }}</div>
|
||||
<div class="match-badges">
|
||||
<span class="badge type">{{ m.typeName }}</span>
|
||||
<span class="badge status" :class="statusClass(m.status)">{{
|
||||
m.statusName
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="match-meta">
|
||||
<span class="store">{{ m.storeName || "-" }}</span>
|
||||
<span class="time">{{ formatTime(m.startTime || m.createdAt) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!loadingMatches && matches.length === 0" class="empty">
|
||||
暂无比赛
|
||||
</div>
|
||||
</div>
|
||||
<div class="scroll-group">
|
||||
<div v-for="m in matches" :key="`m2-${m.id}`" class="match-item">
|
||||
<div class="match-top">
|
||||
<div class="match-name">{{ m.name || m.matchCode }}</div>
|
||||
<div class="match-badges">
|
||||
<span class="badge type">{{ m.typeName }}</span>
|
||||
<span class="badge status" :class="statusClass(m.status)">{{
|
||||
m.statusName
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="match-meta">
|
||||
<span class="store">{{ m.storeName || "-" }}</span>
|
||||
<span class="time">{{ formatTime(m.startTime || m.createdAt) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<span>数据实时同步 · 每30秒刷新</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, nextTick, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import dayjs from "dayjs";
|
||||
import { ArrowDown } from "@element-plus/icons-vue";
|
||||
import { getPublicStores, getPublicRanking, getDisplayMatches } from "@/api/display";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const currentTime = ref(dayjs().format("YYYY-MM-DD HH:mm:ss"));
|
||||
const stores = ref([]);
|
||||
const currentStore = ref(null);
|
||||
const showStoreMenu = ref(false);
|
||||
|
||||
const levels = ref([
|
||||
{ value: 1, label: "Lv1 新锐" },
|
||||
{ value: 2, label: "Lv2 精锐" },
|
||||
{ value: 3, label: "Lv3 高手" },
|
||||
{ value: 4, label: "Lv4 大师" },
|
||||
{ value: 5, label: "Lv5 宗师" },
|
||||
]);
|
||||
|
||||
const boards = ref({});
|
||||
const matches = ref([]);
|
||||
const loadingBoards = ref(false);
|
||||
const loadingMatches = ref(false);
|
||||
|
||||
let timer = null;
|
||||
let refreshTimer = null;
|
||||
let handleWindowClick = null;
|
||||
let boardsRaf = null;
|
||||
let matchesRaf = null;
|
||||
|
||||
const boardsScrollEl = ref(null);
|
||||
const boardsGroup1 = ref(null);
|
||||
const matchesScrollEl = ref(null);
|
||||
const matchesGroup1 = ref(null);
|
||||
|
||||
const boardsScrollY = ref(0);
|
||||
const matchesScrollY = ref(0);
|
||||
|
||||
const updateTime = () => {
|
||||
currentTime.value = dayjs().format("YYYY-MM-DD HH:mm:ss");
|
||||
};
|
||||
|
||||
const formatTime = (t) => {
|
||||
if (!t) return "-";
|
||||
return dayjs(t).format("MM-DD HH:mm");
|
||||
};
|
||||
|
||||
const statusClass = (s) => {
|
||||
if (s === 1) return "ongoing";
|
||||
if (s === 0) return "pending";
|
||||
if (s === 2) return "finished";
|
||||
return "cancelled";
|
||||
};
|
||||
|
||||
const getBoardKey = (gender, level) => `${gender}_${level}`;
|
||||
|
||||
const getBoardList = (gender, level) => {
|
||||
const key = getBoardKey(gender, level);
|
||||
return boards.value[key] || [];
|
||||
};
|
||||
|
||||
const fetchStores = async () => {
|
||||
const res = await getPublicStores();
|
||||
stores.value = res.data?.list || [];
|
||||
|
||||
const urlStoreId = route.query.store_id;
|
||||
if (urlStoreId) {
|
||||
const found = stores.value.find((s) => s.id === parseInt(urlStoreId));
|
||||
if (found) currentStore.value = found;
|
||||
}
|
||||
|
||||
if (!currentStore.value && stores.value.length > 0) {
|
||||
currentStore.value = stores.value[0];
|
||||
}
|
||||
};
|
||||
|
||||
const fetchBoards = async () => {
|
||||
if (!currentStore.value) return;
|
||||
loadingBoards.value = true;
|
||||
try {
|
||||
const tasks = [];
|
||||
for (const level of levels.value) {
|
||||
tasks.push(
|
||||
getPublicRanking({
|
||||
store_id: currentStore.value.id,
|
||||
gender: 1,
|
||||
level: level.value,
|
||||
page: 1,
|
||||
pageSize: 15,
|
||||
is_display: 1,
|
||||
no_count: 1,
|
||||
}).then((res) => ({ gender: 1, level: level.value, list: res.data?.list || [] })),
|
||||
);
|
||||
tasks.push(
|
||||
getPublicRanking({
|
||||
store_id: currentStore.value.id,
|
||||
gender: 2,
|
||||
level: level.value,
|
||||
page: 1,
|
||||
pageSize: 15,
|
||||
is_display: 1,
|
||||
no_count: 1,
|
||||
}).then((res) => ({ gender: 2, level: level.value, list: res.data?.list || [] })),
|
||||
);
|
||||
}
|
||||
|
||||
const result = await Promise.all(tasks);
|
||||
const nextBoards = {};
|
||||
for (const r of result) {
|
||||
nextBoards[getBoardKey(r.gender, r.level)] = r.list;
|
||||
}
|
||||
boards.value = nextBoards;
|
||||
} finally {
|
||||
loadingBoards.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchMatches = async () => {
|
||||
if (!currentStore.value) return;
|
||||
loadingMatches.value = true;
|
||||
try {
|
||||
const res = await getDisplayMatches({
|
||||
store_id: currentStore.value.id,
|
||||
days: 7,
|
||||
limit: 30,
|
||||
});
|
||||
matches.value = res.data || [];
|
||||
} finally {
|
||||
loadingMatches.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const refreshAll = async () => {
|
||||
await Promise.all([fetchBoards(), fetchMatches()]);
|
||||
};
|
||||
|
||||
const stopAutoScroll = () => {
|
||||
if (boardsRaf) cancelAnimationFrame(boardsRaf);
|
||||
if (matchesRaf) cancelAnimationFrame(matchesRaf);
|
||||
boardsRaf = null;
|
||||
matchesRaf = null;
|
||||
};
|
||||
|
||||
const startBoardsScroll = () => {
|
||||
if (!boardsScrollEl.value || !boardsGroup1.value) return;
|
||||
if (loadingBoards.value) return;
|
||||
|
||||
const tick = () => {
|
||||
if (!boardsScrollEl.value || !boardsGroup1.value) {
|
||||
boardsRaf = requestAnimationFrame(tick);
|
||||
return;
|
||||
}
|
||||
if (loadingBoards.value) {
|
||||
boardsRaf = requestAnimationFrame(tick);
|
||||
return;
|
||||
}
|
||||
|
||||
const h = boardsGroup1.value.offsetHeight;
|
||||
const containerH = boardsScrollEl.value.offsetHeight;
|
||||
if (!h || h <= containerH) {
|
||||
boardsScrollY.value = 0;
|
||||
boardsRaf = requestAnimationFrame(tick);
|
||||
return;
|
||||
}
|
||||
|
||||
boardsScrollY.value -= 0.35;
|
||||
if (Math.abs(boardsScrollY.value) >= h) {
|
||||
boardsScrollY.value = 0;
|
||||
}
|
||||
boardsRaf = requestAnimationFrame(tick);
|
||||
};
|
||||
|
||||
if (boardsRaf) cancelAnimationFrame(boardsRaf);
|
||||
boardsRaf = requestAnimationFrame(tick);
|
||||
};
|
||||
|
||||
const startMatchesScroll = () => {
|
||||
if (!matchesScrollEl.value || !matchesGroup1.value) return;
|
||||
if (loadingMatches.value) return;
|
||||
|
||||
const tick = () => {
|
||||
if (!matchesScrollEl.value || !matchesGroup1.value) {
|
||||
matchesRaf = requestAnimationFrame(tick);
|
||||
return;
|
||||
}
|
||||
if (loadingMatches.value) {
|
||||
matchesRaf = requestAnimationFrame(tick);
|
||||
return;
|
||||
}
|
||||
|
||||
const h = matchesGroup1.value.offsetHeight;
|
||||
const containerH = matchesScrollEl.value.offsetHeight;
|
||||
if (!h || h <= containerH) {
|
||||
matchesScrollY.value = 0;
|
||||
matchesRaf = requestAnimationFrame(tick);
|
||||
return;
|
||||
}
|
||||
|
||||
matchesScrollY.value -= 0.4;
|
||||
if (Math.abs(matchesScrollY.value) >= h) {
|
||||
matchesScrollY.value = 0;
|
||||
}
|
||||
matchesRaf = requestAnimationFrame(tick);
|
||||
};
|
||||
|
||||
if (matchesRaf) cancelAnimationFrame(matchesRaf);
|
||||
matchesRaf = requestAnimationFrame(tick);
|
||||
};
|
||||
|
||||
const selectStore = async (store) => {
|
||||
currentStore.value = store;
|
||||
showStoreMenu.value = false;
|
||||
router.replace({ query: { ...route.query, store_id: store.id } });
|
||||
await nextTick();
|
||||
stopAutoScroll();
|
||||
boardsScrollY.value = 0;
|
||||
matchesScrollY.value = 0;
|
||||
refreshAll();
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchStores();
|
||||
await refreshAll();
|
||||
|
||||
timer = setInterval(updateTime, 1000);
|
||||
refreshTimer = setInterval(refreshAll, 30 * 1000);
|
||||
|
||||
handleWindowClick = () => {
|
||||
showStoreMenu.value = false;
|
||||
};
|
||||
window.addEventListener("click", handleWindowClick);
|
||||
|
||||
nextTick(() => {
|
||||
startBoardsScroll();
|
||||
startMatchesScroll();
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) clearInterval(timer);
|
||||
if (refreshTimer) clearInterval(refreshTimer);
|
||||
stopAutoScroll();
|
||||
if (handleWindowClick) window.removeEventListener("click", handleWindowClick);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => loadingBoards.value,
|
||||
(v) => {
|
||||
if (!v) {
|
||||
nextTick(() => startBoardsScroll());
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => loadingMatches.value,
|
||||
(v) => {
|
||||
if (!v) {
|
||||
nextTick(() => startMatchesScroll());
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.ladder-summary-orange {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: #0a0806;
|
||||
color: #fff;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
height: 84px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 40px;
|
||||
background: linear-gradient(to bottom, rgba(18, 12, 8, 0.92), transparent);
|
||||
position: relative;
|
||||
z-index: 50;
|
||||
border-bottom: 1px solid rgba(255, 152, 0, 0.18);
|
||||
}
|
||||
|
||||
.time-box {
|
||||
min-width: 260px;
|
||||
color: #ff9800;
|
||||
font-size: 20px;
|
||||
letter-spacing: 2px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.title-box {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 62%;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
margin: 0;
|
||||
font-size: 40px;
|
||||
letter-spacing: 6px;
|
||||
font-weight: 900;
|
||||
background: linear-gradient(to bottom, #fff, #ffcc80);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
margin-top: 6px;
|
||||
color: rgba(255, 204, 128, 0.75);
|
||||
font-size: 14px;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.right-controls {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.style-switcher {
|
||||
height: 38px;
|
||||
padding: 0 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: rgba(255, 152, 0, 0.1);
|
||||
border: 1px solid rgba(255, 152, 0, 0.28);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: rgba(255, 204, 128, 0.95);
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.style-switcher:hover {
|
||||
background: rgba(255, 152, 0, 0.18);
|
||||
border-color: #ff9800;
|
||||
color: #ff9800;
|
||||
}
|
||||
|
||||
.store-box {
|
||||
background: rgba(255, 152, 0, 0.1);
|
||||
border: 1px solid rgba(255, 152, 0, 0.28);
|
||||
height: 38px;
|
||||
padding: 0 14px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.store-box .label {
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.store-box .value {
|
||||
color: #ff9800;
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
max-width: 220px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.store-box :deep(.el-icon) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.store-box .rotate {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.store-dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
right: 0;
|
||||
min-width: 220px;
|
||||
background: #1a140f;
|
||||
border: 1px solid #ff9800;
|
||||
border-radius: 4px;
|
||||
padding: 10px 0;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.7);
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 10px 16px;
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
background: rgba(255, 152, 0, 0.12);
|
||||
color: #ff9800;
|
||||
}
|
||||
|
||||
.dropdown-item.active {
|
||||
color: #ff9800;
|
||||
background: rgba(255, 152, 0, 0.15);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 360px;
|
||||
gap: 18px;
|
||||
padding: 18px 22px 14px;
|
||||
}
|
||||
|
||||
.boards,
|
||||
.matches {
|
||||
min-height: 0;
|
||||
border: 1px solid rgba(255, 152, 0, 0.14);
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
overflow: hidden;
|
||||
box-shadow: 0 0 30px rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
|
||||
.boards-header,
|
||||
.matches-header {
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
background: rgba(255, 152, 0, 0.08);
|
||||
border-bottom: 1px solid rgba(255, 152, 0, 0.12);
|
||||
}
|
||||
|
||||
.boards-title,
|
||||
.matches-title {
|
||||
font-weight: 900;
|
||||
letter-spacing: 2px;
|
||||
color: #ff9800;
|
||||
}
|
||||
|
||||
.boards-loading,
|
||||
.matches-loading {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.boards-grid {
|
||||
height: calc(100% - 48px);
|
||||
overflow: hidden;
|
||||
padding: 12px 12px 18px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.level-row {
|
||||
display: grid;
|
||||
grid-template-columns: 120px 1fr;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.level-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid rgba(255, 152, 0, 0.14);
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 152, 0, 0.04);
|
||||
color: rgba(255, 204, 128, 0.95);
|
||||
font-weight: 800;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.gender-columns {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.board-card {
|
||||
border: 1px solid rgba(255, 152, 0, 0.12);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
.board-header {
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 12px;
|
||||
background: rgba(255, 152, 0, 0.08);
|
||||
border-bottom: 1px solid rgba(255, 152, 0, 0.12);
|
||||
}
|
||||
|
||||
.board-name {
|
||||
font-weight: 900;
|
||||
color: #ff9800;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.board-meta {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.board-list {
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
.board-item {
|
||||
display: grid;
|
||||
grid-template-columns: 46px 1fr 72px;
|
||||
gap: 10px;
|
||||
padding: 6px 12px;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.board-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.rank {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-weight: 800;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.name {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.score {
|
||||
text-align: right;
|
||||
color: #ff9800;
|
||||
font-weight: 900;
|
||||
font-family: "Arial Black";
|
||||
}
|
||||
|
||||
.matches-list {
|
||||
height: calc(100% - 48px);
|
||||
overflow: hidden;
|
||||
padding: 10px 12px 14px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.match-item {
|
||||
border: 1px solid rgba(255, 152, 0, 0.16);
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border-radius: 10px;
|
||||
padding: 10px 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.match-top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.match-name {
|
||||
font-weight: 900;
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
line-height: 1.2;
|
||||
max-width: 220px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.match-badges {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 12px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.badge.type {
|
||||
color: rgba(255, 204, 128, 0.95);
|
||||
border-color: rgba(255, 152, 0, 0.18);
|
||||
background: rgba(255, 152, 0, 0.08);
|
||||
}
|
||||
|
||||
.badge.status.ongoing {
|
||||
color: #ff9800;
|
||||
border-color: rgba(255, 152, 0, 0.35);
|
||||
background: rgba(255, 152, 0, 0.14);
|
||||
}
|
||||
|
||||
.badge.status.pending {
|
||||
color: rgba(255, 204, 128, 0.95);
|
||||
border-color: rgba(255, 204, 128, 0.2);
|
||||
background: rgba(255, 204, 128, 0.08);
|
||||
}
|
||||
|
||||
.badge.status.finished {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
border-color: rgba(255, 255, 255, 0.12);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.badge.status.cancelled {
|
||||
color: rgba(255, 90, 90, 0.9);
|
||||
border-color: rgba(255, 90, 90, 0.18);
|
||||
background: rgba(255, 90, 90, 0.08);
|
||||
}
|
||||
|
||||
.match-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 12px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer {
|
||||
height: 38px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
border-top: 1px solid rgba(255, 152, 0, 0.12);
|
||||
}
|
||||
|
||||
.scroll-wrapper {
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -19,7 +19,23 @@
|
||||
<el-icon><Monitor /></el-icon>
|
||||
<span>切换 Premium 风格</span>
|
||||
</div>
|
||||
<div class="store-selector" @click="showStoreMenu = !showStoreMenu">
|
||||
<div class="gender-toggle" @click.stop>
|
||||
<div
|
||||
class="toggle-item"
|
||||
:class="{ active: genderMode === 'split' }"
|
||||
@click.stop="setGenderMode('split')">
|
||||
男女分榜
|
||||
</div>
|
||||
<div
|
||||
class="toggle-item"
|
||||
:class="{ active: genderMode === 'all' }"
|
||||
@click.stop="setGenderMode('all')">
|
||||
全部
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="store-selector"
|
||||
@click.stop="showStoreMenu = !showStoreMenu">
|
||||
<span class="store-label">当前场馆:</span>
|
||||
<span class="store-name">{{ currentStore?.name || "请选择" }}</span>
|
||||
<el-icon class="arrow-icon" :class="{ rotate: showStoreMenu }"
|
||||
@ -44,7 +60,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 主体内容区 -->
|
||||
<div class="board-body">
|
||||
<div class="board-body" :class="{ split: genderMode === 'split' }">
|
||||
<!-- 左右装饰线条 -->
|
||||
<div class="side-decoration left"></div>
|
||||
<div class="side-decoration right"></div>
|
||||
@ -58,7 +74,257 @@
|
||||
</transition>
|
||||
|
||||
<!-- 排行榜主体 -->
|
||||
<div class="ranking-list-wrapper">
|
||||
<div
|
||||
v-if="genderMode === 'split'"
|
||||
class="ranking-list-wrapper split-wrapper">
|
||||
<div class="split-column">
|
||||
<div class="gender-title">男子榜</div>
|
||||
<div class="list-header">
|
||||
<div class="col-rank">排名</div>
|
||||
<div class="col-player">选手</div>
|
||||
<div class="col-level">等级</div>
|
||||
<div class="col-matches">场次/胜率</div>
|
||||
<div class="col-score">战力值</div>
|
||||
</div>
|
||||
|
||||
<div class="list-body">
|
||||
<div
|
||||
class="scroll-content"
|
||||
:style="{ transform: `translateY(${scrollYMale}px)` }">
|
||||
<div
|
||||
v-for="player in rankingListMale"
|
||||
:key="'m1-' + player.id"
|
||||
class="ranking-item"
|
||||
:class="'rank-' + player.rank">
|
||||
<div class="col-rank">
|
||||
<div v-if="player.rank <= 3" class="rank-badge">
|
||||
<span class="rank-num">{{ player.rank }}</span>
|
||||
</div>
|
||||
<span v-else class="rank-num">{{ player.rank }}</span>
|
||||
</div>
|
||||
<div class="col-player">
|
||||
<div class="player-info">
|
||||
<el-avatar
|
||||
:size="50"
|
||||
:src="player.avatar"
|
||||
class="player-avatar">
|
||||
{{ player.realName?.[0] || player.nickname?.[0] || "?" }}
|
||||
</el-avatar>
|
||||
<div class="player-names">
|
||||
<span class="real-name">{{ player.realName }}</span>
|
||||
<span class="nickname">{{ player.nickname }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-level">
|
||||
<span :class="['level-tag', 'lv' + player.level]">
|
||||
{{ player.levelName }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-matches">
|
||||
<div class="match-stats">
|
||||
<span class="count">{{ player.matchCount }}场</span>
|
||||
<span class="rate">{{ player.winRate }}% 胜率</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-score">
|
||||
<div class="score-wrapper">
|
||||
<span class="score-value">{{ player.powerScore }}</span>
|
||||
<div class="score-bar">
|
||||
<div
|
||||
class="bar-fill"
|
||||
:style="{
|
||||
width: (player.powerScore / 3000) * 100 + '%',
|
||||
}"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="player in rankingListMale"
|
||||
:key="'m2-' + player.id"
|
||||
class="ranking-item"
|
||||
:class="'rank-' + player.rank">
|
||||
<div class="col-rank">
|
||||
<div v-if="player.rank <= 3" class="rank-badge">
|
||||
<span class="rank-num">{{ player.rank }}</span>
|
||||
</div>
|
||||
<span v-else class="rank-num">{{ player.rank }}</span>
|
||||
</div>
|
||||
<div class="col-player">
|
||||
<div class="player-info">
|
||||
<el-avatar
|
||||
:size="50"
|
||||
:src="player.avatar"
|
||||
class="player-avatar">
|
||||
{{ player.realName?.[0] || player.nickname?.[0] || "?" }}
|
||||
</el-avatar>
|
||||
<div class="player-names">
|
||||
<span class="real-name">{{ player.realName }}</span>
|
||||
<span class="nickname">{{ player.nickname }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-level">
|
||||
<span :class="['level-tag', 'lv' + player.level]">
|
||||
{{ player.levelName }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-matches">
|
||||
<div class="match-stats">
|
||||
<span class="count">{{ player.matchCount }}场</span>
|
||||
<span class="rate">{{ player.winRate }}% 胜率</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-score">
|
||||
<div class="score-wrapper">
|
||||
<span class="score-value">{{ player.powerScore }}</span>
|
||||
<div class="score-bar">
|
||||
<div
|
||||
class="bar-fill"
|
||||
:style="{
|
||||
width: (player.powerScore / 3000) * 100 + '%',
|
||||
}"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!loading && rankingListMale.length === 0"
|
||||
class="empty-state">
|
||||
暂无排行数据
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="split-column">
|
||||
<div class="gender-title">女子榜</div>
|
||||
<div class="list-header">
|
||||
<div class="col-rank">排名</div>
|
||||
<div class="col-player">选手</div>
|
||||
<div class="col-level">等级</div>
|
||||
<div class="col-matches">场次/胜率</div>
|
||||
<div class="col-score">战力值</div>
|
||||
</div>
|
||||
|
||||
<div class="list-body">
|
||||
<div
|
||||
class="scroll-content"
|
||||
:style="{ transform: `translateY(${scrollYFemale}px)` }">
|
||||
<div
|
||||
v-for="player in rankingListFemale"
|
||||
:key="'f1-' + player.id"
|
||||
class="ranking-item"
|
||||
:class="'rank-' + player.rank">
|
||||
<div class="col-rank">
|
||||
<div v-if="player.rank <= 3" class="rank-badge">
|
||||
<span class="rank-num">{{ player.rank }}</span>
|
||||
</div>
|
||||
<span v-else class="rank-num">{{ player.rank }}</span>
|
||||
</div>
|
||||
<div class="col-player">
|
||||
<div class="player-info">
|
||||
<el-avatar
|
||||
:size="50"
|
||||
:src="player.avatar"
|
||||
class="player-avatar">
|
||||
{{ player.realName?.[0] || player.nickname?.[0] || "?" }}
|
||||
</el-avatar>
|
||||
<div class="player-names">
|
||||
<span class="real-name">{{ player.realName }}</span>
|
||||
<span class="nickname">{{ player.nickname }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-level">
|
||||
<span :class="['level-tag', 'lv' + player.level]">
|
||||
{{ player.levelName }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-matches">
|
||||
<div class="match-stats">
|
||||
<span class="count">{{ player.matchCount }}场</span>
|
||||
<span class="rate">{{ player.winRate }}% 胜率</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-score">
|
||||
<div class="score-wrapper">
|
||||
<span class="score-value">{{ player.powerScore }}</span>
|
||||
<div class="score-bar">
|
||||
<div
|
||||
class="bar-fill"
|
||||
:style="{
|
||||
width: (player.powerScore / 3000) * 100 + '%',
|
||||
}"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="player in rankingListFemale"
|
||||
:key="'f2-' + player.id"
|
||||
class="ranking-item"
|
||||
:class="'rank-' + player.rank">
|
||||
<div class="col-rank">
|
||||
<div v-if="player.rank <= 3" class="rank-badge">
|
||||
<span class="rank-num">{{ player.rank }}</span>
|
||||
</div>
|
||||
<span v-else class="rank-num">{{ player.rank }}</span>
|
||||
</div>
|
||||
<div class="col-player">
|
||||
<div class="player-info">
|
||||
<el-avatar
|
||||
:size="50"
|
||||
:src="player.avatar"
|
||||
class="player-avatar">
|
||||
{{ player.realName?.[0] || player.nickname?.[0] || "?" }}
|
||||
</el-avatar>
|
||||
<div class="player-names">
|
||||
<span class="real-name">{{ player.realName }}</span>
|
||||
<span class="nickname">{{ player.nickname }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-level">
|
||||
<span :class="['level-tag', 'lv' + player.level]">
|
||||
{{ player.levelName }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-matches">
|
||||
<div class="match-stats">
|
||||
<span class="count">{{ player.matchCount }}场</span>
|
||||
<span class="rate">{{ player.winRate }}% 胜率</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-score">
|
||||
<div class="score-wrapper">
|
||||
<span class="score-value">{{ player.powerScore }}</span>
|
||||
<div class="score-bar">
|
||||
<div
|
||||
class="bar-fill"
|
||||
:style="{
|
||||
width: (player.powerScore / 3000) * 100 + '%',
|
||||
}"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!loading && rankingListFemale.length === 0"
|
||||
class="empty-state">
|
||||
暂无排行数据
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="ranking-list-wrapper">
|
||||
<div class="list-header">
|
||||
<div class="col-rank">排名</div>
|
||||
<div class="col-player">选手</div>
|
||||
@ -70,7 +336,7 @@
|
||||
<div class="list-body" ref="scrollContainer">
|
||||
<div
|
||||
class="scroll-content"
|
||||
:style="{ transform: `translateY(${scrollY}px)` }">
|
||||
:style="{ transform: `translateY(${scrollYAll}px)` }">
|
||||
<div
|
||||
v-for="(player, index) in rankingList"
|
||||
:key="'p1-' + player.id"
|
||||
@ -217,15 +483,23 @@
|
||||
const currentTime = ref(dayjs().format("YYYY-MM-DD HH:mm:ss"));
|
||||
const stores = ref([]);
|
||||
const currentStore = ref(null);
|
||||
const genderMode = ref(route.query.gender_mode === "all" ? "all" : "split");
|
||||
const rankingList = ref([]);
|
||||
const rankingListMale = ref([]);
|
||||
const rankingListFemale = ref([]);
|
||||
const loading = ref(false);
|
||||
const showStoreMenu = ref(false);
|
||||
const scrollContainer = ref(null);
|
||||
const isRefreshing = ref(false);
|
||||
let timer = null;
|
||||
let refreshTimer = null;
|
||||
const scrollReqId = ref(null);
|
||||
const scrollY = ref(0); // 使用 transform 的偏移量
|
||||
let handleWindowClick = null;
|
||||
const scrollReqIdAll = ref(null);
|
||||
const scrollReqIdMale = ref(null);
|
||||
const scrollReqIdFemale = ref(null);
|
||||
const scrollYAll = ref(0);
|
||||
const scrollYMale = ref(0);
|
||||
const scrollYFemale = ref(0);
|
||||
|
||||
// 更新时间
|
||||
const updateTime = () => {
|
||||
@ -233,31 +507,31 @@
|
||||
};
|
||||
|
||||
// 连续无缝滚动逻辑 (使用 Transform 性能更好)
|
||||
const startContinuousScroll = () => {
|
||||
if (scrollReqId.value) cancelAnimationFrame(scrollReqId.value);
|
||||
const startContinuousScroll = (listRef, scrollYRef, reqIdRef) => {
|
||||
if (reqIdRef.value) cancelAnimationFrame(reqIdRef.value);
|
||||
|
||||
const scroll = () => {
|
||||
if (isRefreshing.value || rankingList.value.length === 0) {
|
||||
scrollReqId.value = requestAnimationFrame(scroll);
|
||||
if (isRefreshing.value || listRef.value.length === 0) {
|
||||
reqIdRef.value = requestAnimationFrame(scroll);
|
||||
return;
|
||||
}
|
||||
|
||||
// 每一帧移动的像素 (降低速度,使其更易阅读)
|
||||
scrollY.value -= 0.6;
|
||||
scrollYRef.value -= 0.6;
|
||||
|
||||
// 计算单份数据的高度:100条 * (90px行高 + 12px间距)
|
||||
const singleSetHeight = rankingList.value.length * 102;
|
||||
const singleSetHeight = listRef.value.length * 102;
|
||||
|
||||
// 如果滚完了一整套数据,瞬间重置回 0
|
||||
if (Math.abs(scrollY.value) >= singleSetHeight) {
|
||||
scrollY.value = 0;
|
||||
if (Math.abs(scrollYRef.value) >= singleSetHeight) {
|
||||
scrollYRef.value = 0;
|
||||
}
|
||||
|
||||
scrollReqId.value = requestAnimationFrame(scroll);
|
||||
reqIdRef.value = requestAnimationFrame(scroll);
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
scrollReqId.value = requestAnimationFrame(scroll);
|
||||
reqIdRef.value = requestAnimationFrame(scroll);
|
||||
}, 500);
|
||||
};
|
||||
|
||||
@ -289,28 +563,78 @@
|
||||
}
|
||||
};
|
||||
|
||||
const setGenderMode = (mode) => {
|
||||
if (mode !== "split" && mode !== "all") return;
|
||||
if (genderMode.value === mode) return;
|
||||
genderMode.value = mode;
|
||||
router.replace({ query: { ...route.query, gender_mode: mode } });
|
||||
fetchRanking();
|
||||
};
|
||||
|
||||
// 获取排名数据
|
||||
const fetchRanking = async () => {
|
||||
if (!currentStore.value) return;
|
||||
|
||||
isRefreshing.value = true;
|
||||
try {
|
||||
if (genderMode.value === "split") {
|
||||
const [maleRes, femaleRes] = await Promise.all([
|
||||
getPublicRanking({
|
||||
store_id: currentStore.value.id,
|
||||
gender: 1,
|
||||
pageSize: 100,
|
||||
is_display: 1,
|
||||
}),
|
||||
getPublicRanking({
|
||||
store_id: currentStore.value.id,
|
||||
gender: 2,
|
||||
pageSize: 100,
|
||||
is_display: 1,
|
||||
}),
|
||||
]);
|
||||
|
||||
rankingListMale.value = maleRes.data?.list || [];
|
||||
rankingListFemale.value = femaleRes.data?.list || [];
|
||||
scrollYMale.value = 0;
|
||||
scrollYFemale.value = 0;
|
||||
|
||||
if (
|
||||
rankingListMale.value.length > 0 ||
|
||||
rankingListFemale.value.length > 0
|
||||
) {
|
||||
nextTick(() => {
|
||||
if (rankingListMale.value.length > 0) {
|
||||
startContinuousScroll(
|
||||
rankingListMale,
|
||||
scrollYMale,
|
||||
scrollReqIdMale,
|
||||
);
|
||||
}
|
||||
if (rankingListFemale.value.length > 0) {
|
||||
startContinuousScroll(
|
||||
rankingListFemale,
|
||||
scrollYFemale,
|
||||
scrollReqIdFemale,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const res = await getPublicRanking({
|
||||
store_id: currentStore.value.id,
|
||||
pageSize: 100, // 大屏展示前100名
|
||||
is_display: 1, // 告知后端是大屏展示,可以忽略每月最低场次限制
|
||||
});
|
||||
|
||||
// 如果数据没变,不重置滚动位置
|
||||
const newList = res.data?.list || [];
|
||||
rankingList.value = newList;
|
||||
rankingList.value = res.data?.list || [];
|
||||
scrollYAll.value = 0;
|
||||
|
||||
// 数据加载后开始滚动
|
||||
if (rankingList.value.length > 0) {
|
||||
nextTick(() => {
|
||||
startContinuousScroll();
|
||||
startContinuousScroll(rankingList, scrollYAll, scrollReqIdAll);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("获取排名失败:", err);
|
||||
} finally {
|
||||
@ -326,7 +650,13 @@
|
||||
currentStore.value = store;
|
||||
showStoreMenu.value = false;
|
||||
// 更新 URL 方便分享/刷新
|
||||
router.replace({ query: { ...route.query, store_id: store.id } });
|
||||
router.replace({
|
||||
query: {
|
||||
...route.query,
|
||||
store_id: store.id,
|
||||
gender_mode: genderMode.value,
|
||||
},
|
||||
});
|
||||
fetchRanking();
|
||||
};
|
||||
|
||||
@ -336,24 +666,27 @@
|
||||
refreshTimer = setInterval(fetchRanking, 30 * 1000); // 30秒刷新一次,避免过于频繁影响滚动感
|
||||
|
||||
// 点击外部关闭菜单
|
||||
window.addEventListener("click", () => {
|
||||
handleWindowClick = () => {
|
||||
showStoreMenu.value = false;
|
||||
});
|
||||
};
|
||||
window.addEventListener("click", handleWindowClick);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) clearInterval(timer);
|
||||
if (refreshTimer) clearInterval(refreshTimer);
|
||||
if (scrollReqId.value) cancelAnimationFrame(scrollReqId.value);
|
||||
window.removeEventListener("click", () => {
|
||||
showStoreMenu.value = false;
|
||||
});
|
||||
if (scrollReqIdAll.value) cancelAnimationFrame(scrollReqIdAll.value);
|
||||
if (scrollReqIdMale.value) cancelAnimationFrame(scrollReqIdMale.value);
|
||||
if (scrollReqIdFemale.value) cancelAnimationFrame(scrollReqIdFemale.value);
|
||||
if (handleWindowClick) {
|
||||
window.removeEventListener("click", handleWindowClick);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听路由参数变化(比如用户直接修改 URL)
|
||||
watch(
|
||||
() => route.query.store_id,
|
||||
(newId) => {
|
||||
() => [route.query.store_id, route.query.gender_mode],
|
||||
([newId, newMode]) => {
|
||||
if (newId && stores.value.length > 0) {
|
||||
const found = stores.value.find((s) => s.id === parseInt(newId));
|
||||
if (found && found.id !== currentStore.value?.id) {
|
||||
@ -361,6 +694,12 @@
|
||||
fetchRanking();
|
||||
}
|
||||
}
|
||||
|
||||
const mode = newMode === "all" ? "all" : "split";
|
||||
if (mode !== genderMode.value) {
|
||||
genderMode.value = mode;
|
||||
fetchRanking();
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
@ -428,11 +767,12 @@
|
||||
justify-content: space-between;
|
||||
padding: 0 40px;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
z-index: 100;
|
||||
background: linear-gradient(to bottom, rgba(10, 25, 51, 0.8), transparent);
|
||||
|
||||
.header-left {
|
||||
flex: 1;
|
||||
flex: 0 0 auto;
|
||||
min-width: 240px;
|
||||
.current-time {
|
||||
font-size: 24px;
|
||||
font-weight: 300;
|
||||
@ -443,9 +783,14 @@
|
||||
}
|
||||
|
||||
.header-center {
|
||||
flex: 2;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 60%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
|
||||
.title-bg {
|
||||
position: relative;
|
||||
@ -482,11 +827,14 @@
|
||||
}
|
||||
|
||||
.header-right {
|
||||
flex: 1;
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
flex-wrap: nowrap;
|
||||
position: relative;
|
||||
z-index: 30;
|
||||
|
||||
.style-switcher {
|
||||
display: flex;
|
||||
@ -494,12 +842,26 @@
|
||||
gap: 8px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(0, 242, 255, 0.2);
|
||||
padding: 8px 15px;
|
||||
height: 38px;
|
||||
padding: 0 14px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: #a5d8ff;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s;
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
|
||||
span {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
:deep(.el-icon) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 242, 255, 0.1);
|
||||
@ -509,10 +871,48 @@
|
||||
}
|
||||
}
|
||||
|
||||
.gender-toggle {
|
||||
display: flex;
|
||||
height: 38px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(0, 242, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
|
||||
.toggle-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
font-size: 14px;
|
||||
color: #a5d8ff;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: all 0.2s;
|
||||
border-right: 1px solid rgba(0, 242, 255, 0.12);
|
||||
line-height: 1;
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #00f2ff;
|
||||
background: rgba(0, 242, 255, 0.12);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 242, 255, 0.08);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.store-selector {
|
||||
background: rgba(0, 242, 255, 0.1);
|
||||
border: 1px solid rgba(0, 242, 255, 0.3);
|
||||
padding: 8px 20px;
|
||||
height: 38px;
|
||||
padding: 0 14px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
@ -520,6 +920,10 @@
|
||||
gap: 12px;
|
||||
position: relative;
|
||||
transition: all 0.3s;
|
||||
box-sizing: border-box;
|
||||
user-select: none;
|
||||
z-index: 40;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 242, 255, 0.2);
|
||||
@ -529,15 +933,24 @@
|
||||
.store-label {
|
||||
font-size: 14px;
|
||||
color: #a5d8ff;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.store-name {
|
||||
font-size: 20px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #00f2ff;
|
||||
max-width: 180px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.arrow-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.3s;
|
||||
&.rotate {
|
||||
transform: rotate(180deg);
|
||||
@ -546,7 +959,7 @@
|
||||
|
||||
.store-menu {
|
||||
position: absolute;
|
||||
top: 110%;
|
||||
top: calc(100% + 8px);
|
||||
right: 0;
|
||||
width: 200px;
|
||||
background: #0a1933;
|
||||
@ -554,12 +967,13 @@
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
|
||||
padding: 10px 0;
|
||||
z-index: 100;
|
||||
z-index: 200;
|
||||
|
||||
.store-item {
|
||||
padding: 12px 20px;
|
||||
transition: all 0.3s;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 242, 255, 0.1);
|
||||
@ -586,6 +1000,12 @@
|
||||
flex-direction: column;
|
||||
min-height: 0; /* 确保子元素 flex 正确 */
|
||||
|
||||
&.split {
|
||||
flex-direction: row;
|
||||
gap: 24px;
|
||||
padding: 20px 40px;
|
||||
}
|
||||
|
||||
.side-decoration {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
@ -641,7 +1061,7 @@
|
||||
letter-spacing: 3px;
|
||||
border-bottom: 2px solid rgba(0, 242, 255, 0.3);
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
z-index: 1;
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
|
||||
|
||||
div {
|
||||
@ -662,6 +1082,44 @@
|
||||
}
|
||||
}
|
||||
|
||||
.split-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 24px;
|
||||
padding: 18px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.split-column {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.gender-title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #00f2ff;
|
||||
letter-spacing: 2px;
|
||||
margin: 0 0 10px 6px;
|
||||
text-shadow: 0 0 12px rgba(0, 242, 255, 0.25);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
border-radius: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.list-body {
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.01);
|
||||
border: 1px solid rgba(0, 242, 255, 0.12);
|
||||
overflow: hidden;
|
||||
padding: 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.scroll-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@ -21,7 +21,21 @@
|
||||
<el-icon><Monitor /></el-icon>
|
||||
<span>切换经典风格</span>
|
||||
</div>
|
||||
<div class="store-box" @click="showStoreMenu = !showStoreMenu">
|
||||
<div class="gender-toggle" @click.stop>
|
||||
<div
|
||||
class="toggle-item"
|
||||
:class="{ active: genderMode === 'split' }"
|
||||
@click.stop="setGenderMode('split')">
|
||||
男女分榜
|
||||
</div>
|
||||
<div
|
||||
class="toggle-item"
|
||||
:class="{ active: genderMode === 'all' }"
|
||||
@click.stop="setGenderMode('all')">
|
||||
全部
|
||||
</div>
|
||||
</div>
|
||||
<div class="store-box" @click.stop="showStoreMenu = !showStoreMenu">
|
||||
<span class="label">场馆:</span>
|
||||
<span class="value">{{ currentStore?.name || "未选择" }}</span>
|
||||
<el-icon :class="{ rotate: showStoreMenu }"><ArrowDown /></el-icon>
|
||||
@ -42,7 +56,282 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-content">
|
||||
<div class="main-content" :class="{ split: genderMode === 'split' }">
|
||||
<div v-if="genderMode === 'split'" class="split-board">
|
||||
<div class="split-column">
|
||||
<div class="split-label">男子榜</div>
|
||||
<div class="split-hof">
|
||||
<div class="podium mini">
|
||||
<div v-if="rankingListMale[1]" class="podium-card rank-2">
|
||||
<div class="rank-label">NO.2</div>
|
||||
<div class="avatar-wrapper">
|
||||
<el-avatar
|
||||
:size="56"
|
||||
:src="rankingListMale[1].avatar"
|
||||
class="avatar">
|
||||
{{ rankingListMale[1].realName?.[0] || "?" }}
|
||||
</el-avatar>
|
||||
<div class="crown">🥈</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="name">{{ rankingListMale[1].realName }}</div>
|
||||
<div class="score">{{ rankingListMale[1].powerScore }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="rankingListMale[0]" class="podium-card rank-1">
|
||||
<div class="rank-label">NO.1</div>
|
||||
<div class="avatar-wrapper">
|
||||
<el-avatar
|
||||
:size="66"
|
||||
:src="rankingListMale[0].avatar"
|
||||
class="avatar">
|
||||
{{ rankingListMale[0].realName?.[0] || "?" }}
|
||||
</el-avatar>
|
||||
<div class="crown">👑</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="name">{{ rankingListMale[0].realName }}</div>
|
||||
<div class="score">{{ rankingListMale[0].powerScore }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="rankingListMale[2]" class="podium-card rank-3">
|
||||
<div class="rank-label">NO.3</div>
|
||||
<div class="avatar-wrapper">
|
||||
<el-avatar
|
||||
:size="56"
|
||||
:src="rankingListMale[2].avatar"
|
||||
class="avatar">
|
||||
{{ rankingListMale[2].realName?.[0] || "?" }}
|
||||
</el-avatar>
|
||||
<div class="crown">🥉</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="name">{{ rankingListMale[2].realName }}</div>
|
||||
<div class="score">{{ rankingListMale[2].powerScore }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="contenders-zone split">
|
||||
<div class="panel-header">
|
||||
<div class="panel-title">MEN</div>
|
||||
<div class="refresh-tag" v-if="isRefreshing">
|
||||
<el-icon class="is-loading"><Refresh /></el-icon> 同步中
|
||||
</div>
|
||||
</div>
|
||||
<div class="scroll-container">
|
||||
<div class="scroll-mask">
|
||||
<div
|
||||
class="scroll-wrapper"
|
||||
:style="{ transform: `translateY(${scrollYMale}px)` }">
|
||||
<div class="grid-layout" ref="groupMale">
|
||||
<div
|
||||
v-for="player in rankingListMale.slice(3)"
|
||||
:key="'mc1-' + player.id"
|
||||
class="contender-card">
|
||||
<div class="card-rank">{{ player.rank }}</div>
|
||||
<div class="card-avatar">
|
||||
<el-avatar :size="46" :src="player.avatar">
|
||||
{{ player.realName?.[0] || "?" }}
|
||||
</el-avatar>
|
||||
</div>
|
||||
<div class="card-info">
|
||||
<div class="card-name">{{ player.realName }}</div>
|
||||
<div class="card-meta">
|
||||
<span class="card-level">{{ player.levelName }}</span>
|
||||
<span class="card-wins"
|
||||
>{{
|
||||
Math.round(
|
||||
(player.matchCount * player.winRate) / 100,
|
||||
)
|
||||
}}胜</span
|
||||
>
|
||||
<span class="card-winrate"
|
||||
>{{ player.winRate }}%</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-score">{{ player.powerScore }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid-layout">
|
||||
<div
|
||||
v-for="player in rankingListMale.slice(3)"
|
||||
:key="'mc2-' + player.id"
|
||||
class="contender-card">
|
||||
<div class="card-rank">{{ player.rank }}</div>
|
||||
<div class="card-avatar">
|
||||
<el-avatar :size="46" :src="player.avatar">
|
||||
{{ player.realName?.[0] || "?" }}
|
||||
</el-avatar>
|
||||
</div>
|
||||
<div class="card-info">
|
||||
<div class="card-name">{{ player.realName }}</div>
|
||||
<div class="card-meta">
|
||||
<span class="card-level">{{ player.levelName }}</span>
|
||||
<span class="card-wins"
|
||||
>{{
|
||||
Math.round(
|
||||
(player.matchCount * player.winRate) / 100,
|
||||
)
|
||||
}}胜</span
|
||||
>
|
||||
<span class="card-winrate"
|
||||
>{{ player.winRate }}%</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-score">{{ player.powerScore }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bottom-fade-overlay"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="split-column">
|
||||
<div class="split-label">女子榜</div>
|
||||
<div class="split-hof">
|
||||
<div class="podium mini">
|
||||
<div v-if="rankingListFemale[1]" class="podium-card rank-2">
|
||||
<div class="rank-label">NO.2</div>
|
||||
<div class="avatar-wrapper">
|
||||
<el-avatar
|
||||
:size="56"
|
||||
:src="rankingListFemale[1].avatar"
|
||||
class="avatar">
|
||||
{{ rankingListFemale[1].realName?.[0] || "?" }}
|
||||
</el-avatar>
|
||||
<div class="crown">🥈</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="name">{{ rankingListFemale[1].realName }}</div>
|
||||
<div class="score">{{ rankingListFemale[1].powerScore }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="rankingListFemale[0]" class="podium-card rank-1">
|
||||
<div class="rank-label">NO.1</div>
|
||||
<div class="avatar-wrapper">
|
||||
<el-avatar
|
||||
:size="66"
|
||||
:src="rankingListFemale[0].avatar"
|
||||
class="avatar">
|
||||
{{ rankingListFemale[0].realName?.[0] || "?" }}
|
||||
</el-avatar>
|
||||
<div class="crown">👑</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="name">{{ rankingListFemale[0].realName }}</div>
|
||||
<div class="score">{{ rankingListFemale[0].powerScore }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="rankingListFemale[2]" class="podium-card rank-3">
|
||||
<div class="rank-label">NO.3</div>
|
||||
<div class="avatar-wrapper">
|
||||
<el-avatar
|
||||
:size="56"
|
||||
:src="rankingListFemale[2].avatar"
|
||||
class="avatar">
|
||||
{{ rankingListFemale[2].realName?.[0] || "?" }}
|
||||
</el-avatar>
|
||||
<div class="crown">🥉</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="name">{{ rankingListFemale[2].realName }}</div>
|
||||
<div class="score">{{ rankingListFemale[2].powerScore }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="contenders-zone split">
|
||||
<div class="panel-header">
|
||||
<div class="panel-title">WOMEN</div>
|
||||
<div class="refresh-tag" v-if="isRefreshing">
|
||||
<el-icon class="is-loading"><Refresh /></el-icon> 同步中
|
||||
</div>
|
||||
</div>
|
||||
<div class="scroll-container">
|
||||
<div class="scroll-mask">
|
||||
<div
|
||||
class="scroll-wrapper"
|
||||
:style="{ transform: `translateY(${scrollYFemale}px)` }">
|
||||
<div class="grid-layout" ref="groupFemale">
|
||||
<div
|
||||
v-for="player in rankingListFemale.slice(3)"
|
||||
:key="'fc1-' + player.id"
|
||||
class="contender-card">
|
||||
<div class="card-rank">{{ player.rank }}</div>
|
||||
<div class="card-avatar">
|
||||
<el-avatar :size="46" :src="player.avatar">
|
||||
{{ player.realName?.[0] || "?" }}
|
||||
</el-avatar>
|
||||
</div>
|
||||
<div class="card-info">
|
||||
<div class="card-name">{{ player.realName }}</div>
|
||||
<div class="card-meta">
|
||||
<span class="card-level">{{ player.levelName }}</span>
|
||||
<span class="card-wins"
|
||||
>{{
|
||||
Math.round(
|
||||
(player.matchCount * player.winRate) / 100,
|
||||
)
|
||||
}}胜</span
|
||||
>
|
||||
<span class="card-winrate"
|
||||
>{{ player.winRate }}%</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-score">{{ player.powerScore }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid-layout">
|
||||
<div
|
||||
v-for="player in rankingListFemale.slice(3)"
|
||||
:key="'fc2-' + player.id"
|
||||
class="contender-card">
|
||||
<div class="card-rank">{{ player.rank }}</div>
|
||||
<div class="card-avatar">
|
||||
<el-avatar :size="46" :src="player.avatar">
|
||||
{{ player.realName?.[0] || "?" }}
|
||||
</el-avatar>
|
||||
</div>
|
||||
<div class="card-info">
|
||||
<div class="card-name">{{ player.realName }}</div>
|
||||
<div class="card-meta">
|
||||
<span class="card-level">{{ player.levelName }}</span>
|
||||
<span class="card-wins"
|
||||
>{{
|
||||
Math.round(
|
||||
(player.matchCount * player.winRate) / 100,
|
||||
)
|
||||
}}胜</span
|
||||
>
|
||||
<span class="card-winrate"
|
||||
>{{ player.winRate }}%</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-score">{{ player.powerScore }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bottom-fade-overlay"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<!-- 左侧:名人堂 (Top 3) -->
|
||||
<div class="hall-of-fame">
|
||||
<div class="panel-title">HALL OF FAME</div>
|
||||
@ -51,7 +340,10 @@
|
||||
<div v-if="rankingList[1]" class="podium-card rank-2">
|
||||
<div class="rank-label">NO.2</div>
|
||||
<div class="avatar-wrapper">
|
||||
<el-avatar :size="70" :src="rankingList[1].avatar" class="avatar">
|
||||
<el-avatar
|
||||
:size="70"
|
||||
:src="rankingList[1].avatar"
|
||||
class="avatar">
|
||||
{{ rankingList[1].realName?.[0] || "?" }}
|
||||
</el-avatar>
|
||||
<div class="crown">🥈</div>
|
||||
@ -107,7 +399,10 @@
|
||||
<div v-if="rankingList[2]" class="podium-card rank-3">
|
||||
<div class="rank-label">NO.3</div>
|
||||
<div class="avatar-wrapper">
|
||||
<el-avatar :size="70" :src="rankingList[2].avatar" class="avatar">
|
||||
<el-avatar
|
||||
:size="70"
|
||||
:src="rankingList[2].avatar"
|
||||
class="avatar">
|
||||
{{ rankingList[2].realName?.[0] || "?" }}
|
||||
</el-avatar>
|
||||
<div class="crown">🥉</div>
|
||||
@ -209,6 +504,7 @@
|
||||
<div class="bottom-fade-overlay"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 底部提示 -->
|
||||
@ -234,47 +530,52 @@
|
||||
const stores = ref([]);
|
||||
const currentStore = ref(null);
|
||||
const rankingList = ref([]);
|
||||
const genderMode = ref(route.query.gender_mode === "all" ? "all" : "split");
|
||||
const rankingListMale = ref([]);
|
||||
const rankingListFemale = ref([]);
|
||||
const loading = ref(false);
|
||||
const showStoreMenu = ref(false);
|
||||
const scrollContainer = ref(null);
|
||||
const isRefreshing = ref(false);
|
||||
let timer = null;
|
||||
let refreshTimer = null;
|
||||
let handleWindowClick = null;
|
||||
const group1 = ref(null);
|
||||
const scrollReqId = ref(null);
|
||||
const scrollY = ref(0);
|
||||
const groupMale = ref(null);
|
||||
const groupFemale = ref(null);
|
||||
const scrollReqIdMale = ref(null);
|
||||
const scrollReqIdFemale = ref(null);
|
||||
const scrollYMale = ref(0);
|
||||
const scrollYFemale = ref(0);
|
||||
|
||||
const updateTime = () => {
|
||||
currentTime.value = dayjs().format("HH:mm:ss");
|
||||
};
|
||||
|
||||
const startContinuousScroll = () => {
|
||||
if (scrollReqId.value) cancelAnimationFrame(scrollReqId.value);
|
||||
const startContinuousScroll = (listRef, scrollYRef, reqIdRef, groupRef) => {
|
||||
if (reqIdRef.value) cancelAnimationFrame(reqIdRef.value);
|
||||
|
||||
const scroll = () => {
|
||||
if (
|
||||
isRefreshing.value ||
|
||||
rankingList.value.length <= 3 ||
|
||||
!group1.value
|
||||
) {
|
||||
scrollReqId.value = requestAnimationFrame(scroll);
|
||||
if (isRefreshing.value || listRef.value.length <= 3 || !groupRef.value) {
|
||||
reqIdRef.value = requestAnimationFrame(scroll);
|
||||
return;
|
||||
}
|
||||
|
||||
scrollY.value -= 0.6;
|
||||
scrollYRef.value -= 0.6;
|
||||
|
||||
// 直接获取第一组数据的高度,最准确
|
||||
const singleSetHeight = group1.value.offsetHeight + 12; // 高度 + gap
|
||||
const singleSetHeight = groupRef.value.offsetHeight + 12;
|
||||
|
||||
if (Math.abs(scrollY.value) >= singleSetHeight) {
|
||||
scrollY.value = 0;
|
||||
if (Math.abs(scrollYRef.value) >= singleSetHeight) {
|
||||
scrollYRef.value = 0;
|
||||
}
|
||||
|
||||
scrollReqId.value = requestAnimationFrame(scroll);
|
||||
reqIdRef.value = requestAnimationFrame(scroll);
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
scrollReqId.value = requestAnimationFrame(scroll);
|
||||
reqIdRef.value = requestAnimationFrame(scroll);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
@ -296,18 +597,75 @@
|
||||
}
|
||||
};
|
||||
|
||||
const setGenderMode = (mode) => {
|
||||
if (mode !== "split" && mode !== "all") return;
|
||||
if (genderMode.value === mode) return;
|
||||
genderMode.value = mode;
|
||||
router.replace({ query: { ...route.query, gender_mode: mode } });
|
||||
fetchRanking();
|
||||
};
|
||||
|
||||
const fetchRanking = async () => {
|
||||
if (!currentStore.value) return;
|
||||
isRefreshing.value = true;
|
||||
try {
|
||||
if (genderMode.value === "split") {
|
||||
const [maleRes, femaleRes] = await Promise.all([
|
||||
getPublicRanking({
|
||||
store_id: currentStore.value.id,
|
||||
gender: 1,
|
||||
pageSize: 100,
|
||||
is_display: 1,
|
||||
}),
|
||||
getPublicRanking({
|
||||
store_id: currentStore.value.id,
|
||||
gender: 2,
|
||||
pageSize: 100,
|
||||
is_display: 1,
|
||||
}),
|
||||
]);
|
||||
|
||||
rankingListMale.value = maleRes.data?.list || [];
|
||||
rankingListFemale.value = femaleRes.data?.list || [];
|
||||
scrollYMale.value = 0;
|
||||
scrollYFemale.value = 0;
|
||||
|
||||
if (
|
||||
rankingListMale.value.length > 3 ||
|
||||
rankingListFemale.value.length > 3
|
||||
) {
|
||||
nextTick(() => {
|
||||
if (rankingListMale.value.length > 3) {
|
||||
startContinuousScroll(
|
||||
rankingListMale,
|
||||
scrollYMale,
|
||||
scrollReqIdMale,
|
||||
groupMale,
|
||||
);
|
||||
}
|
||||
if (rankingListFemale.value.length > 3) {
|
||||
startContinuousScroll(
|
||||
rankingListFemale,
|
||||
scrollYFemale,
|
||||
scrollReqIdFemale,
|
||||
groupFemale,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const res = await getPublicRanking({
|
||||
store_id: currentStore.value.id,
|
||||
pageSize: 100,
|
||||
is_display: 1,
|
||||
});
|
||||
rankingList.value = res.data?.list || [];
|
||||
scrollY.value = 0;
|
||||
if (rankingList.value.length > 3) {
|
||||
nextTick(() => startContinuousScroll());
|
||||
nextTick(() => {
|
||||
startContinuousScroll(rankingList, scrollY, scrollReqId, group1);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Fetch ranking error:", err);
|
||||
@ -322,7 +680,13 @@
|
||||
const selectStore = (store) => {
|
||||
currentStore.value = store;
|
||||
showStoreMenu.value = false;
|
||||
router.replace({ query: { ...route.query, store_id: store.id } });
|
||||
router.replace({
|
||||
query: {
|
||||
...route.query,
|
||||
store_id: store.id,
|
||||
gender_mode: genderMode.value,
|
||||
},
|
||||
});
|
||||
fetchRanking();
|
||||
};
|
||||
|
||||
@ -330,16 +694,40 @@
|
||||
fetchStores();
|
||||
timer = setInterval(updateTime, 1000);
|
||||
refreshTimer = setInterval(fetchRanking, 30 * 1000);
|
||||
window.addEventListener("click", () => {
|
||||
handleWindowClick = () => {
|
||||
showStoreMenu.value = false;
|
||||
});
|
||||
};
|
||||
window.addEventListener("click", handleWindowClick);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) clearInterval(timer);
|
||||
if (refreshTimer) clearInterval(refreshTimer);
|
||||
if (scrollReqId.value) cancelAnimationFrame(scrollReqId.value);
|
||||
if (scrollReqIdMale.value) cancelAnimationFrame(scrollReqIdMale.value);
|
||||
if (scrollReqIdFemale.value) cancelAnimationFrame(scrollReqIdFemale.value);
|
||||
if (handleWindowClick) {
|
||||
window.removeEventListener("click", handleWindowClick);
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => [route.query.store_id, route.query.gender_mode],
|
||||
([newStoreId, newMode]) => {
|
||||
const mode = newMode === "all" ? "all" : "split";
|
||||
if (mode !== genderMode.value) {
|
||||
genderMode.value = mode;
|
||||
fetchRanking();
|
||||
}
|
||||
|
||||
if (!newStoreId) return;
|
||||
const found = stores.value.find((s) => s.id === parseInt(newStoreId));
|
||||
if (found && found.id !== currentStore.value?.id) {
|
||||
currentStore.value = found;
|
||||
fetchRanking();
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@ -431,7 +819,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 30px;
|
||||
width: 400px; /* 增加宽度以容纳两个控件 */
|
||||
width: 560px;
|
||||
justify-content: flex-end;
|
||||
|
||||
.style-switcher {
|
||||
@ -440,12 +828,15 @@
|
||||
gap: 8px;
|
||||
background: rgba(255, 152, 0, 0.1);
|
||||
border: 1px solid rgba(255, 152, 0, 0.3);
|
||||
padding: 8px 15px;
|
||||
height: 38px;
|
||||
padding: 0 14px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: #ffcc80;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s;
|
||||
box-sizing: border-box;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 152, 0, 0.2);
|
||||
@ -455,6 +846,42 @@
|
||||
}
|
||||
}
|
||||
|
||||
.gender-toggle {
|
||||
display: flex;
|
||||
height: 38px;
|
||||
background: rgba(255, 152, 0, 0.08);
|
||||
border: 1px solid rgba(255, 152, 0, 0.3);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
|
||||
.toggle-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
font-size: 14px;
|
||||
color: #ffcc80;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: all 0.2s;
|
||||
border-right: 1px solid rgba(255, 152, 0, 0.18);
|
||||
white-space: nowrap;
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #ff9800;
|
||||
background: rgba(255, 152, 0, 0.18);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 152, 0, 0.12);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.store-box {
|
||||
text-align: right;
|
||||
cursor: pointer;
|
||||
@ -499,6 +926,149 @@
|
||||
}
|
||||
}
|
||||
|
||||
.main-content.split {
|
||||
display: flex;
|
||||
gap: 26px;
|
||||
}
|
||||
|
||||
.split-board {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
gap: 26px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.split-column {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.split-label {
|
||||
font-size: 14px;
|
||||
color: rgba(255, 204, 128, 0.9);
|
||||
letter-spacing: 2px;
|
||||
font-weight: 800;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.split-hof {
|
||||
border: 1px solid rgba(255, 152, 0, 0.18);
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border-radius: 12px;
|
||||
padding: 10px 10px 14px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.podium.mini {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.podium-card {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 152, 0, 0.12) 0%,
|
||||
rgba(255, 152, 0, 0.03) 100%
|
||||
);
|
||||
border: 1px solid rgba(255, 152, 0, 0.18);
|
||||
border-radius: 12px;
|
||||
padding: 12px 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
|
||||
&.rank-1 {
|
||||
flex: 1.15;
|
||||
border-color: rgba(255, 215, 0, 0.45);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 215, 0, 0.16) 0%,
|
||||
rgba(255, 152, 0, 0.04) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.rank-label {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 900;
|
||||
color: rgba(255, 152, 0, 0.65);
|
||||
letter-spacing: 1px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.avatar-wrapper {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
.avatar {
|
||||
border: 2px solid #ff9800;
|
||||
background: #33190a;
|
||||
font-weight: 900;
|
||||
font-size: 18px;
|
||||
}
|
||||
.crown {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
right: -8px;
|
||||
font-size: 22px;
|
||||
z-index: 5;
|
||||
filter: drop-shadow(0 0 8px rgba(0, 0, 0, 0.5));
|
||||
}
|
||||
}
|
||||
|
||||
.card-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: 900;
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.score {
|
||||
font-weight: 900;
|
||||
font-family: "Arial Black";
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
color: #ffcc80;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.podium.mini {
|
||||
margin-top: 8px;
|
||||
transform: scale(0.96);
|
||||
transform-origin: top center;
|
||||
}
|
||||
|
||||
.contenders-zone.split {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Main Content */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
|
||||
@ -132,7 +132,7 @@
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="等级" prop="level">
|
||||
<el-select v-model="editForm.level" placeholder="选择等级">
|
||||
<el-select v-model="editForm.level" placeholder="选择等级" @change="handleLevelChange">
|
||||
<el-option label="Lv1 新锐" :value="1" />
|
||||
<el-option label="Lv2 精锐" :value="2" />
|
||||
<el-option label="Lv3 高手" :value="3" />
|
||||
@ -160,6 +160,14 @@ import { getLadderUsers, createLadderUser, updateLadderUser, deleteLadderUser, g
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const LEVEL_POWER_DEFAULTS = {
|
||||
1: 1000,
|
||||
2: 1200,
|
||||
3: 1500,
|
||||
4: 1800,
|
||||
5: 2200
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const tableData = ref([])
|
||||
const stores = ref([])
|
||||
@ -241,6 +249,14 @@ const handleEdit = (row) => {
|
||||
showEditDialog.value = true
|
||||
}
|
||||
|
||||
const handleLevelChange = (level) => {
|
||||
if (editForm.value.id) return
|
||||
const next = LEVEL_POWER_DEFAULTS[level]
|
||||
if (next !== undefined) {
|
||||
editForm.value.power_score = next
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
await editFormRef.value?.validate()
|
||||
|
||||
|
||||
@ -8,10 +8,18 @@
|
||||
<!-- 搜索表单 -->
|
||||
<el-form :inline="true" class="search-form">
|
||||
<el-form-item>
|
||||
<el-input v-model="searchForm.keyword" placeholder="昵称/手机号/会员码" clearable style="width: 200px" />
|
||||
<el-input
|
||||
v-model="searchForm.keyword"
|
||||
placeholder="昵称/手机号/会员码"
|
||||
clearable
|
||||
style="width: 200px" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-select v-model="searchForm.status" placeholder="状态" clearable style="width: 100px">
|
||||
<el-select
|
||||
v-model="searchForm.status"
|
||||
placeholder="状态"
|
||||
clearable
|
||||
style="width: 100px">
|
||||
<el-option label="正常" :value="1" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
</el-select>
|
||||
@ -28,10 +36,12 @@
|
||||
<el-table-column label="用户" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<div class="user-cell">
|
||||
<el-avatar :size="40" :src="row.avatar">{{ row.nickname?.[0] }}</el-avatar>
|
||||
<el-avatar :size="40" :src="row.avatar">{{
|
||||
row.nickname?.[0]
|
||||
}}</el-avatar>
|
||||
<div class="user-info">
|
||||
<span class="nickname">{{ row.nickname || '-' }}</span>
|
||||
<span class="phone">{{ row.phone || '未绑定手机' }}</span>
|
||||
<span class="nickname">{{ row.nickname || "-" }}</span>
|
||||
<span class="phone">{{ row.phone || "未绑定手机" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -40,7 +50,7 @@
|
||||
<el-table-column prop="gender" label="性别" width="80">
|
||||
<template #default="{ row }">
|
||||
<span :class="['gender-tag', row.gender === 1 ? 'male' : 'female']">
|
||||
{{ row.gender === 1 ? '男' : row.gender === 2 ? '女' : '未知' }}
|
||||
{{ row.gender === 1 ? "男" : row.gender === 2 ? "女" : "未知" }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@ -48,7 +58,7 @@
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
|
||||
{{ row.status === 1 ? '正常' : '禁用' }}
|
||||
{{ row.status === 1 ? "正常" : "禁用" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@ -57,16 +67,20 @@
|
||||
{{ formatDate(row.createdAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150" fixed="right">
|
||||
<el-table-column label="操作" width="220" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div class="table-actions">
|
||||
<el-button type="primary" link @click="handleView(row)">查看</el-button>
|
||||
<el-button type="primary" link @click="handleView(row)"
|
||||
>查看</el-button
|
||||
>
|
||||
<el-button type="primary" link @click="handleAddLadderUser(row)"
|
||||
>添加为天梯用户</el-button
|
||||
>
|
||||
<el-button
|
||||
:type="row.status === 1 ? 'danger' : 'success'"
|
||||
link
|
||||
@click="handleToggleStatus(row)"
|
||||
>
|
||||
{{ row.status === 1 ? '禁用' : '启用' }}
|
||||
@click="handleToggleStatus(row)">
|
||||
{{ row.status === 1 ? "禁用" : "启用" }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
@ -82,31 +96,53 @@
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="fetchData"
|
||||
@current-change="fetchData"
|
||||
/>
|
||||
@current-change="fetchData" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户详情弹窗 -->
|
||||
<el-dialog v-model="showDetailDialog" title="用户详情" width="600px">
|
||||
<el-descriptions :column="2" border v-if="currentUser">
|
||||
<el-descriptions-item label="ID">{{ currentUser.id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="昵称">{{ currentUser.nickname }}</el-descriptions-item>
|
||||
<el-descriptions-item label="手机号">{{ currentUser.phone || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="会员码">{{ currentUser.memberCode }}</el-descriptions-item>
|
||||
<el-descriptions-item label="性别">{{ currentUser.gender === 1 ? '男' : currentUser.gender === 2 ? '女' : '未知' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="积分">{{ currentUser.totalPoints }}</el-descriptions-item>
|
||||
<el-descriptions-item label="注册时间" :span="2">{{ formatDate(currentUser.createdAt) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="ID">{{
|
||||
currentUser.id
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="昵称">{{
|
||||
currentUser.nickname
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="手机号">{{
|
||||
currentUser.phone || "-"
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="会员码">{{
|
||||
currentUser.memberCode
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="性别">{{
|
||||
currentUser.gender === 1
|
||||
? "男"
|
||||
: currentUser.gender === 2
|
||||
? "女"
|
||||
: "未知"
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="积分">{{
|
||||
currentUser.totalPoints
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="注册时间" :span="2">{{
|
||||
formatDate(currentUser.createdAt)
|
||||
}}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div v-if="currentUser?.ladderUsers?.length" style="margin-top: 20px">
|
||||
<h4>天梯信息</h4>
|
||||
<el-table :data="currentUser.ladderUsers" size="small" style="margin-top: 10px">
|
||||
<el-table
|
||||
:data="currentUser.ladderUsers"
|
||||
size="small"
|
||||
style="margin-top: 10px">
|
||||
<el-table-column prop="storeName" label="门店" />
|
||||
<el-table-column prop="realName" label="姓名" />
|
||||
<el-table-column prop="level" label="等级">
|
||||
<template #default="{ row }">
|
||||
<span :class="['level-tag', 'lv' + row.level]">Lv{{ row.level }}</span>
|
||||
<span :class="['level-tag', 'lv' + row.level]"
|
||||
>Lv{{ row.level }}</span
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="powerScore" label="战力值" />
|
||||
@ -114,70 +150,212 @@
|
||||
</el-table>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 添加为天梯用户弹窗 -->
|
||||
<el-dialog v-model="showAddLadderDialog" title="新增天梯用户" width="520px">
|
||||
<el-form
|
||||
:model="addLadderForm"
|
||||
:rules="addLadderRules"
|
||||
ref="addLadderFormRef"
|
||||
label-width="100px">
|
||||
<el-form-item
|
||||
v-if="userStore.isSuperAdmin"
|
||||
label="门店"
|
||||
prop="store_id">
|
||||
<el-select v-model="addLadderForm.store_id" placeholder="选择门店">
|
||||
<el-option
|
||||
v-for="store in stores"
|
||||
:key="store.id"
|
||||
:label="store.name"
|
||||
:value="store.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号" prop="phone">
|
||||
<el-input
|
||||
v-model="addLadderForm.phone"
|
||||
placeholder="请输入用户手机号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="真实姓名" prop="real_name">
|
||||
<el-input
|
||||
v-model="addLadderForm.real_name"
|
||||
placeholder="请输入真实姓名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="性别" prop="gender">
|
||||
<el-radio-group v-model="addLadderForm.gender">
|
||||
<el-radio :value="1">男</el-radio>
|
||||
<el-radio :value="2">女</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="等级" prop="level">
|
||||
<el-select
|
||||
v-model="addLadderForm.level"
|
||||
placeholder="选择等级"
|
||||
@change="handleAddLadderLevelChange">
|
||||
<el-option label="Lv1 新锐" :value="1" />
|
||||
<el-option label="Lv2 精锐" :value="2" />
|
||||
<el-option label="Lv3 高手" :value="3" />
|
||||
<el-option label="Lv4 大师" :value="4" />
|
||||
<el-option label="Lv5 宗师" :value="5" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="战力值" prop="power_score">
|
||||
<el-input-number
|
||||
v-model="addLadderForm.power_score"
|
||||
:min="0"
|
||||
:max="9999" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showAddLadderDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleCreateLadderUser"
|
||||
>保存</el-button
|
||||
>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import dayjs from 'dayjs'
|
||||
import { getUsers, getUserDetail, updateUserStatus } from '@/api/admin'
|
||||
import { ref, onMounted, computed } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import dayjs from "dayjs";
|
||||
import {
|
||||
getUsers,
|
||||
getUserDetail,
|
||||
updateUserStatus,
|
||||
createLadderUser,
|
||||
getStores,
|
||||
} from "@/api/admin";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
|
||||
const loading = ref(false)
|
||||
const tableData = ref([])
|
||||
const searchForm = ref({ keyword: '', status: '' })
|
||||
const pagination = ref({ page: 1, pageSize: 20, total: 0 })
|
||||
const userStore = useUserStore();
|
||||
|
||||
const showDetailDialog = ref(false)
|
||||
const currentUser = ref(null)
|
||||
const LEVEL_POWER_DEFAULTS = {
|
||||
1: 1000,
|
||||
2: 1200,
|
||||
3: 1500,
|
||||
4: 1800,
|
||||
5: 2200,
|
||||
};
|
||||
|
||||
const formatDate = (date) => dayjs(date).format('YYYY-MM-DD HH:mm')
|
||||
const loading = ref(false);
|
||||
const tableData = ref([]);
|
||||
const searchForm = ref({ keyword: "", status: "" });
|
||||
const pagination = ref({ page: 1, pageSize: 20, total: 0 });
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
const showDetailDialog = ref(false);
|
||||
const currentUser = ref(null);
|
||||
|
||||
const stores = ref([]);
|
||||
const showAddLadderDialog = ref(false);
|
||||
const addLadderFormRef = ref();
|
||||
const addLadderForm = ref({
|
||||
store_id: "",
|
||||
phone: "",
|
||||
real_name: "",
|
||||
gender: 1,
|
||||
level: 1,
|
||||
power_score: 1000,
|
||||
});
|
||||
|
||||
const addLadderRules = computed(() => ({
|
||||
...(userStore.isSuperAdmin
|
||||
? {
|
||||
store_id: [
|
||||
{ required: true, message: "请选择门店", trigger: "change" },
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
phone: [{ required: true, message: "请输入手机号", trigger: "blur" }],
|
||||
real_name: [{ required: true, message: "请输入真实姓名", trigger: "blur" }],
|
||||
gender: [{ required: true, message: "请选择性别", trigger: "change" }],
|
||||
level: [{ required: true, message: "请选择等级", trigger: "change" }],
|
||||
power_score: [{ required: true, message: "请输入战力值", trigger: "blur" }],
|
||||
}));
|
||||
|
||||
const formatDate = (date) => dayjs(date).format("YYYY-MM-DD HH:mm");
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getUsers({
|
||||
...searchForm.value,
|
||||
page: pagination.value.page,
|
||||
pageSize: pagination.value.pageSize
|
||||
})
|
||||
tableData.value = res.data.list
|
||||
pagination.value.total = res.data.pagination.total
|
||||
pageSize: pagination.value.pageSize,
|
||||
});
|
||||
tableData.value = res.data.list;
|
||||
pagination.value.total = res.data.pagination.total;
|
||||
} finally {
|
||||
loading.value = false
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const resetSearch = () => {
|
||||
searchForm.value = { keyword: '', status: '' }
|
||||
pagination.value.page = 1
|
||||
fetchData()
|
||||
}
|
||||
const resetSearch = () => {
|
||||
searchForm.value = { keyword: "", status: "" };
|
||||
pagination.value.page = 1;
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const handleView = async (row) => {
|
||||
const res = await getUserDetail(row.id)
|
||||
currentUser.value = res.data
|
||||
showDetailDialog.value = true
|
||||
}
|
||||
const handleView = async (row) => {
|
||||
const res = await getUserDetail(row.id);
|
||||
currentUser.value = res.data;
|
||||
showDetailDialog.value = true;
|
||||
};
|
||||
|
||||
const handleToggleStatus = (row) => {
|
||||
const newStatus = row.status === 1 ? 0 : 1
|
||||
const action = newStatus === 1 ? '启用' : '禁用'
|
||||
const handleToggleStatus = (row) => {
|
||||
const newStatus = row.status === 1 ? 0 : 1;
|
||||
const action = newStatus === 1 ? "启用" : "禁用";
|
||||
|
||||
ElMessageBox.confirm(`确定要${action}该用户吗?`, '提示', {
|
||||
type: 'warning'
|
||||
ElMessageBox.confirm(`确定要${action}该用户吗?`, "提示", {
|
||||
type: "warning",
|
||||
}).then(async () => {
|
||||
await updateUserStatus(row.id, { status: newStatus })
|
||||
ElMessage.success(`${action}成功`)
|
||||
fetchData()
|
||||
})
|
||||
}
|
||||
await updateUserStatus(row.id, { status: newStatus });
|
||||
ElMessage.success(`${action}成功`);
|
||||
fetchData();
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(fetchData)
|
||||
const fetchStores = async () => {
|
||||
if (userStore.isSuperAdmin) {
|
||||
const res = await getStores({ pageSize: 100 });
|
||||
stores.value = res.data.list;
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddLadderUser = (row) => {
|
||||
addLadderForm.value = {
|
||||
store_id: userStore.isSuperAdmin ? "" : userStore.userInfo?.storeId || "",
|
||||
phone: row.phone || "",
|
||||
real_name: row.nickname || "",
|
||||
gender: row.gender === 1 || row.gender === 2 ? row.gender : 1,
|
||||
level: 1,
|
||||
power_score: 1000,
|
||||
};
|
||||
showAddLadderDialog.value = true;
|
||||
};
|
||||
|
||||
const handleAddLadderLevelChange = (level) => {
|
||||
const next = LEVEL_POWER_DEFAULTS[level];
|
||||
if (next !== undefined) {
|
||||
addLadderForm.value.power_score = next;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateLadderUser = async () => {
|
||||
await addLadderFormRef.value?.validate();
|
||||
await createLadderUser(addLadderForm.value);
|
||||
ElMessage.success("创建成功");
|
||||
showAddLadderDialog.value = false;
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchStores();
|
||||
fetchData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.user-cell {
|
||||
.user-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
@ -195,5 +373,5 @@ onMounted(fetchData)
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -91,21 +91,30 @@ App({
|
||||
this.globalData.userInfo = loginRes.data.data.userInfo;
|
||||
|
||||
// 处理天梯用户信息
|
||||
if (loginRes.data.data.userInfo.ladderUsers && loginRes.data.data.userInfo.ladderUsers.length > 0) {
|
||||
if (
|
||||
loginRes.data.data.userInfo.ladderUsers &&
|
||||
loginRes.data.data.userInfo.ladderUsers.length > 0
|
||||
) {
|
||||
// 如果有当前门店,优先选择当前门店的天梯用户
|
||||
if (this.globalData.currentStore && this.globalData.currentStore.storeId) {
|
||||
const currentStoreLadderUser = loginRes.data.data.userInfo.ladderUsers.find(
|
||||
lu => lu.storeId === this.globalData.currentStore.storeId
|
||||
if (
|
||||
this.globalData.currentStore &&
|
||||
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];
|
||||
this.globalData.ladderUser =
|
||||
loginRes.data.data.userInfo.ladderUsers[0];
|
||||
}
|
||||
} else {
|
||||
// 没有当前门店,取第一个天梯用户
|
||||
this.globalData.ladderUser = loginRes.data.data.userInfo.ladderUsers[0];
|
||||
this.globalData.ladderUser =
|
||||
loginRes.data.data.userInfo.ladderUsers[0];
|
||||
}
|
||||
} else {
|
||||
// 没有天梯用户
|
||||
@ -139,9 +148,12 @@ App({
|
||||
// 处理天梯用户信息
|
||||
if (res.data.ladderUsers && res.data.ladderUsers.length > 0) {
|
||||
// 如果有当前门店,优先选择当前门店的天梯用户
|
||||
if (this.globalData.currentStore && this.globalData.currentStore.storeId) {
|
||||
if (
|
||||
this.globalData.currentStore &&
|
||||
this.globalData.currentStore.storeId
|
||||
) {
|
||||
const currentStoreLadderUser = res.data.ladderUsers.find(
|
||||
lu => lu.storeId === this.globalData.currentStore.storeId
|
||||
(lu) => lu.storeId === this.globalData.currentStore.storeId,
|
||||
);
|
||||
if (currentStoreLadderUser) {
|
||||
this.globalData.ladderUser = currentStoreLadderUser;
|
||||
@ -183,9 +195,13 @@ App({
|
||||
this.getLadderUser(res.data.storeId);
|
||||
} else if (res.data && res.data.storeId) {
|
||||
// 如果当前门店没有 ladderUserId,但用户信息中有该门店的天梯用户,使用它
|
||||
if (this.globalData.userInfo && this.globalData.userInfo.ladderUsers) {
|
||||
const currentStoreLadderUser = this.globalData.userInfo.ladderUsers.find(
|
||||
lu => lu.storeId === res.data.storeId
|
||||
if (
|
||||
this.globalData.userInfo &&
|
||||
this.globalData.userInfo.ladderUsers
|
||||
) {
|
||||
const currentStoreLadderUser =
|
||||
this.globalData.userInfo.ladderUsers.find(
|
||||
(lu) => lu.storeId === res.data.storeId,
|
||||
);
|
||||
if (currentStoreLadderUser) {
|
||||
this.globalData.ladderUser = currentStoreLadderUser;
|
||||
@ -211,9 +227,13 @@ App({
|
||||
this.getLadderUser(res.data.storeId);
|
||||
} else if (res.data && res.data.storeId) {
|
||||
// 如果当前门店没有 ladderUserId,但用户信息中有该门店的天梯用户,使用它
|
||||
if (this.globalData.userInfo && this.globalData.userInfo.ladderUsers) {
|
||||
const currentStoreLadderUser = this.globalData.userInfo.ladderUsers.find(
|
||||
lu => lu.storeId === res.data.storeId
|
||||
if (
|
||||
this.globalData.userInfo &&
|
||||
this.globalData.userInfo.ladderUsers
|
||||
) {
|
||||
const currentStoreLadderUser =
|
||||
this.globalData.userInfo.ladderUsers.find(
|
||||
(lu) => lu.storeId === res.data.storeId,
|
||||
);
|
||||
if (currentStoreLadderUser) {
|
||||
this.globalData.ladderUser = currentStoreLadderUser;
|
||||
@ -243,7 +263,7 @@ App({
|
||||
this.globalData.ladderUser = null;
|
||||
}
|
||||
return res.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
@ -301,7 +321,10 @@ App({
|
||||
const currentPage = pages[pages.length - 1];
|
||||
|
||||
// 如果当前页面有处理挑战请求的方法,调用它
|
||||
if (currentPage && typeof currentPage.handleChallengeRequest === 'function') {
|
||||
if (
|
||||
currentPage &&
|
||||
typeof currentPage.handleChallengeRequest === "function"
|
||||
) {
|
||||
currentPage.handleChallengeRequest(challengeData);
|
||||
} else {
|
||||
// 否则使用系统弹框
|
||||
@ -317,22 +340,24 @@ App({
|
||||
match_id: challengeData.matchId,
|
||||
accept: res.confirm,
|
||||
},
|
||||
"POST"
|
||||
).then(() => {
|
||||
"POST",
|
||||
)
|
||||
.then(() => {
|
||||
if (res.confirm) {
|
||||
wx.showToast({ title: '已接受挑战', icon: 'success' });
|
||||
wx.showToast({ title: "已接受挑战", icon: "success" });
|
||||
// 跳转到挑战赛详情
|
||||
setTimeout(() => {
|
||||
wx.navigateTo({
|
||||
url: `/pages/match/challenge-detail/index?id=${challengeData.matchId}`
|
||||
url: `/pages/match/challenge-detail/index?id=${challengeData.matchId}`,
|
||||
});
|
||||
}, 1500);
|
||||
} else {
|
||||
wx.showToast({ title: '已拒绝挑战', icon: 'success' });
|
||||
wx.showToast({ title: "已拒绝挑战", icon: "success" });
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error('响应挑战失败:', err);
|
||||
wx.showToast({ title: '操作失败', icon: 'none' });
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("响应挑战失败:", err);
|
||||
wx.showToast({ title: "操作失败", icon: "none" });
|
||||
});
|
||||
},
|
||||
});
|
||||
@ -340,19 +365,22 @@ App({
|
||||
break;
|
||||
case "challenge_accepted":
|
||||
// 挑战被接受
|
||||
wx.showToast({ title: '对方已接受挑战', icon: 'success' });
|
||||
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') {
|
||||
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' });
|
||||
wx.showToast({ title: "对方已拒绝挑战", icon: "none" });
|
||||
break;
|
||||
case "score_confirm_request":
|
||||
// 收到比分确认请求
|
||||
@ -368,7 +396,7 @@ App({
|
||||
game_id: data.data.gameId,
|
||||
confirm: res.confirm,
|
||||
},
|
||||
"POST"
|
||||
"POST",
|
||||
);
|
||||
},
|
||||
});
|
||||
@ -387,6 +415,12 @@ App({
|
||||
// 封装请求
|
||||
request(url, data = {}, method = "GET") {
|
||||
return new Promise((resolve, reject) => {
|
||||
const showErrorToast = (message) => {
|
||||
const title =
|
||||
(message && String(message).trim()) || "网络异常,请稍后重试";
|
||||
wx.showToast({ title, icon: "none" });
|
||||
};
|
||||
|
||||
wx.request({
|
||||
url: `${this.globalData.baseUrl}${url}`,
|
||||
method,
|
||||
@ -401,14 +435,18 @@ App({
|
||||
// 登录过期
|
||||
this.globalData.token = null;
|
||||
wx.removeStorageSync("token");
|
||||
showErrorToast("登录已过期,请重新登录");
|
||||
wx.reLaunch({ url: "/pages/user/index" });
|
||||
reject(res.data);
|
||||
reject({ message: res.data.message || "登录已过期", ...res.data });
|
||||
} else {
|
||||
wx.showToast({ title: res.data.message, icon: "none" });
|
||||
reject(res.data);
|
||||
showErrorToast(res.data.message || "请求失败");
|
||||
reject({ message: res.data.message || "请求失败", ...res.data });
|
||||
}
|
||||
},
|
||||
fail: reject,
|
||||
fail: (err) => {
|
||||
showErrorToast("网络异常,请检查网络后重试");
|
||||
reject({ message: "网络异常,请检查网络后重试", err });
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
@ -1,15 +1,17 @@
|
||||
const app = getApp();
|
||||
const util = require("../../utils/util");
|
||||
|
||||
const ALL_PAGE_SIZE = 5000;
|
||||
|
||||
Page({
|
||||
data: {
|
||||
currentStore: null,
|
||||
gender: "",
|
||||
list: [],
|
||||
loading: false,
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
hasMore: true,
|
||||
locating: false,
|
||||
myLadderUserId: null,
|
||||
pageSize: ALL_PAGE_SIZE,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
@ -26,9 +28,8 @@ Page({
|
||||
if (newStore && newStore.storeId !== oldStoreId) {
|
||||
this.setData({
|
||||
currentStore: newStore,
|
||||
page: 1,
|
||||
hasMore: true,
|
||||
list: [],
|
||||
myLadderUserId: null,
|
||||
});
|
||||
this.fetchData();
|
||||
} else if (app.globalData.storeChanged) {
|
||||
@ -36,27 +37,20 @@ Page({
|
||||
app.globalData.storeChanged = false;
|
||||
this.setData({
|
||||
currentStore: newStore,
|
||||
page: 1,
|
||||
hasMore: true,
|
||||
list: [],
|
||||
myLadderUserId: null,
|
||||
});
|
||||
this.fetchData();
|
||||
}
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
this.setData({ page: 1, hasMore: true });
|
||||
this.setData({ list: [], myLadderUserId: null });
|
||||
this.fetchData().then(() => {
|
||||
wx.stopPullDownRefresh();
|
||||
});
|
||||
},
|
||||
|
||||
onReachBottom() {
|
||||
if (this.data.hasMore && !this.data.loading) {
|
||||
this.loadMore();
|
||||
}
|
||||
},
|
||||
|
||||
async initData() {
|
||||
// 检查是否已登录(有 token)
|
||||
if (!app.globalData.token) {
|
||||
@ -88,14 +82,14 @@ Page({
|
||||
const res = await app.request("/api/ladder/ranking", {
|
||||
store_id: this.data.currentStore.storeId,
|
||||
gender: this.data.gender,
|
||||
page: this.data.page,
|
||||
page: 1,
|
||||
pageSize: this.data.pageSize,
|
||||
no_count: 1,
|
||||
});
|
||||
|
||||
const list = res.data.list || [];
|
||||
this.setData({
|
||||
list: this.data.page === 1 ? list : this.data.list.concat(list),
|
||||
hasMore: list.length >= this.data.pageSize,
|
||||
list,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("获取排名失败:", e);
|
||||
@ -104,14 +98,9 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
loadMore() {
|
||||
this.setData({ page: this.data.page + 1 });
|
||||
this.fetchData();
|
||||
},
|
||||
|
||||
setGender(e) {
|
||||
const gender = e.currentTarget.dataset.gender;
|
||||
this.setData({ gender, page: 1, hasMore: true });
|
||||
this.setData({ gender, list: [], myLadderUserId: null });
|
||||
this.fetchData();
|
||||
},
|
||||
|
||||
@ -119,6 +108,85 @@ Page({
|
||||
wx.navigateTo({ url: "/pages/store/index" });
|
||||
},
|
||||
|
||||
scrollToMeInMiddle(tryCount = 0) {
|
||||
const id = this.data.myLadderUserId;
|
||||
if (!id) return;
|
||||
|
||||
const selector = `#player-${id}`;
|
||||
const systemInfo = wx.getSystemInfoSync();
|
||||
const windowHeight =
|
||||
systemInfo && systemInfo.windowHeight ? systemInfo.windowHeight : 0;
|
||||
|
||||
wx.createSelectorQuery()
|
||||
.select(selector)
|
||||
.boundingClientRect()
|
||||
.selectViewport()
|
||||
.scrollOffset()
|
||||
.exec((res) => {
|
||||
const rect = res && res[0] ? res[0] : null;
|
||||
const scroll = res && res[1] ? res[1] : null;
|
||||
if (!rect || !scroll || typeof scroll.scrollTop !== "number") {
|
||||
if (tryCount < 10) {
|
||||
setTimeout(() => {
|
||||
this.scrollToMeInMiddle(tryCount + 1);
|
||||
}, 100);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const targetScrollTop =
|
||||
scroll.scrollTop + rect.top - windowHeight / 2 + rect.height / 2;
|
||||
|
||||
wx.pageScrollTo({
|
||||
scrollTop: Math.max(0, targetScrollTop),
|
||||
duration: 300,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
async locateMe() {
|
||||
if (!this.data.currentStore || !this.data.currentStore.storeId) return;
|
||||
if (this.data.locating) return;
|
||||
|
||||
this.setData({ locating: true });
|
||||
try {
|
||||
const res = await app.request("/api/ladder/my-rank", {
|
||||
store_id: this.data.currentStore.storeId,
|
||||
gender: this.data.gender,
|
||||
});
|
||||
|
||||
const data = res && res.data ? res.data : null;
|
||||
if (!data) return;
|
||||
|
||||
if (data.qualified === false) {
|
||||
wx.showToast({
|
||||
title: `本月场次不足(${data.monthlyMatchCount}/${data.minMonthlyMatches})`,
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data.ladderUserId || !data.page) {
|
||||
return;
|
||||
}
|
||||
|
||||
const shouldRefetch = !this.data.list || this.data.list.length === 0;
|
||||
this.setData({ myLadderUserId: data.ladderUserId });
|
||||
|
||||
if (shouldRefetch) {
|
||||
await this.fetchData();
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.scrollToMeInMiddle();
|
||||
}, 50);
|
||||
} catch (e) {
|
||||
console.error("定位我的排名失败:", e);
|
||||
} finally {
|
||||
this.setData({ locating: false });
|
||||
}
|
||||
},
|
||||
|
||||
viewPlayer(e) {
|
||||
const player = e.currentTarget.dataset.player;
|
||||
const id = player && player.id ? player.id : e.currentTarget.dataset.id;
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
<!--天梯排名页面 - 浅色高级感设计-->
|
||||
<!-- 天梯排名页面 - 浅色高级感设计 -->
|
||||
<view class="page-container">
|
||||
<!-- 顶部装饰背景 -->
|
||||
<view class="hero-section">
|
||||
<view class="hero-pattern"></view>
|
||||
<view class="hero-pattern-2"></view>
|
||||
|
||||
<!-- 门店信息 -->
|
||||
<view class="store-header">
|
||||
<view class="store-info">
|
||||
@ -16,62 +15,43 @@
|
||||
<text class="change-store-arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 页面标题 -->
|
||||
<view class="page-header animate-fadeInUp" style="animation-delay: 0.05s">
|
||||
<text class="page-title">天梯排名</text>
|
||||
<text class="page-subtitle">挑战自我,超越巅峰</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 主要内容区域 -->
|
||||
<view class="main-content">
|
||||
<!-- 性别筛选标签 - 吸附在顶部 -->
|
||||
<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 class="filter-items">
|
||||
<view class="filter-item {{gender === '' ? 'active' : ''}}" bindtap="setGender" data-gender="">
|
||||
全部
|
||||
</view>
|
||||
<view
|
||||
class="filter-item {{gender === '1' ? 'active' : ''}}"
|
||||
bindtap="setGender"
|
||||
data-gender="1"
|
||||
>
|
||||
<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-item {{gender === '2' ? 'active' : ''}}" bindtap="setGender" data-gender="2">
|
||||
女子
|
||||
</view>
|
||||
</view>
|
||||
<view class="locate-btn {{locating ? 'disabled' : ''}}" bindtap="locateMe">
|
||||
{{locating ? '定位中' : '定位我'}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 排名列表 -->
|
||||
<view class="ranking-list">
|
||||
<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}}"
|
||||
data-player="{{item}}"
|
||||
>
|
||||
<view id="player-{{item.id}}" class="ranking-item stagger-item {{index < 3 ? 'top-rank' : ''}} {{item.id === myLadderUserId ? 'is-me' : ''}} animate-fadeInUp" wx:for="{{list}}" wx:key="id" bindtap="viewPlayer" data-id="{{item.id}}" data-player="{{item}}">
|
||||
<!-- 排名徽章 -->
|
||||
<view class="rank-badge {{item.rank === 1 ? 'top1' : item.rank === 2 ? 'top2' : item.rank === 3 ? 'top3' : 'normal'}}">
|
||||
<text>{{item.rank}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 选手头像 -->
|
||||
<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>
|
||||
@ -80,7 +60,6 @@
|
||||
<text class="player-stats">胜率 {{item.winRate}}%</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 战力值 -->
|
||||
<view class="player-power">
|
||||
<text class="power-value">{{item.powerScore}}</text>
|
||||
@ -88,21 +67,18 @@
|
||||
</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>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view wx:if="{{loading}}" class="loading-state">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 到底提示 -->
|
||||
<view wx:if="{{list.length > 0 && !hasMore && !loading}}" class="load-more">
|
||||
<view wx:if="{{list.length > 0 && !loading}}" class="load-more">
|
||||
<text>— 已显示全部选手 —</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@ -132,6 +132,13 @@
|
||||
|
||||
/* 筛选标签栏 */
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.filter-items {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
}
|
||||
@ -159,6 +166,28 @@
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
.locate-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 20rpx 26rpx;
|
||||
background: var(--bg-white);
|
||||
border-radius: var(--radius-full);
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
color: var(--primary);
|
||||
box-shadow: var(--shadow-sm);
|
||||
border: 1rpx solid var(--border-primary);
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.locate-btn:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
.locate-btn.disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* 排名列表 */
|
||||
.ranking-list {
|
||||
padding: 0 24rpx 40rpx;
|
||||
@ -186,11 +215,20 @@
|
||||
}
|
||||
|
||||
.ranking-item.top-rank {
|
||||
background: linear-gradient(135deg, var(--primary-soft) 0%, var(--bg-white) 100%);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--primary-soft) 0%,
|
||||
var(--bg-white) 100%
|
||||
);
|
||||
border: 2rpx solid var(--border-primary);
|
||||
box-shadow: var(--shadow-primary);
|
||||
}
|
||||
|
||||
.ranking-item.is-me {
|
||||
border: 2rpx solid rgba(255, 107, 53, 0.55);
|
||||
box-shadow: 0 10rpx 28rpx rgba(255, 107, 53, 0.18);
|
||||
}
|
||||
|
||||
/* 排名徽章 */
|
||||
.rank-badge {
|
||||
width: 56rpx;
|
||||
@ -206,8 +244,8 @@
|
||||
}
|
||||
|
||||
.rank-badge.top1 {
|
||||
background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
|
||||
color: #8B4513;
|
||||
background: linear-gradient(135deg, #ffd700 0%, #ffa500 100%);
|
||||
color: #8b4513;
|
||||
box-shadow: 0 6rpx 20rpx rgba(255, 215, 0, 0.5);
|
||||
font-size: 32rpx;
|
||||
width: 64rpx;
|
||||
@ -215,8 +253,8 @@
|
||||
}
|
||||
|
||||
.rank-badge.top2 {
|
||||
background: linear-gradient(135deg, #E8E8E8 0%, #C0C0C0 100%);
|
||||
color: #4A4A4A;
|
||||
background: linear-gradient(135deg, #e8e8e8 0%, #c0c0c0 100%);
|
||||
color: #4a4a4a;
|
||||
box-shadow: 0 4rpx 16rpx rgba(192, 192, 192, 0.5);
|
||||
font-size: 32rpx;
|
||||
width: 64rpx;
|
||||
@ -224,7 +262,7 @@
|
||||
}
|
||||
|
||||
.rank-badge.top3 {
|
||||
background: linear-gradient(135deg, #CD853F 0%, #B8860B 100%);
|
||||
background: linear-gradient(135deg, #cd853f 0%, #b8860b 100%);
|
||||
color: #fff;
|
||||
box-shadow: 0 4rpx 16rpx rgba(205, 133, 63, 0.5);
|
||||
font-size: 32rpx;
|
||||
@ -233,7 +271,11 @@
|
||||
}
|
||||
|
||||
.rank-badge.normal {
|
||||
background: linear-gradient(135deg, var(--bg-soft) 0%, var(--bg-card-hover) 100%);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--bg-soft) 0%,
|
||||
var(--bg-card-hover) 100%
|
||||
);
|
||||
color: var(--text-secondary);
|
||||
font-weight: 600;
|
||||
}
|
||||
@ -284,11 +326,26 @@
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.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; }
|
||||
.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;
|
||||
}
|
||||
|
||||
.player-stats {
|
||||
font-size: 24rpx;
|
||||
@ -360,4 +417,3 @@
|
||||
color: var(--text-muted);
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
|
||||
@ -13,8 +13,26 @@ Page({
|
||||
profileForm: {
|
||||
avatar: "",
|
||||
nickname: "",
|
||||
gender: 0,
|
||||
},
|
||||
isEditMode: false, // true: 编辑模式,false: 完善模式(登录时)
|
||||
|
||||
showGenderModal: false,
|
||||
registerGender: 0,
|
||||
},
|
||||
|
||||
normalizeLadderUser(ladderUser) {
|
||||
if (!ladderUser) return null;
|
||||
const matchCount = Number(ladderUser.matchCount || 0);
|
||||
const winCount = Number(ladderUser.winCount || 0);
|
||||
const loseCount = Math.max(matchCount - winCount, 0);
|
||||
const winRate = matchCount > 0 ? Math.round((winCount / matchCount) * 100) : 0;
|
||||
return Object.assign({}, ladderUser, {
|
||||
matchCount,
|
||||
winCount,
|
||||
loseCount,
|
||||
winRate,
|
||||
});
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
@ -30,7 +48,7 @@ Page({
|
||||
// 同步最新数据
|
||||
this.setData({
|
||||
userInfo: app.globalData.userInfo,
|
||||
ladderUser: app.globalData.ladderUser,
|
||||
ladderUser: this.normalizeLadderUser(app.globalData.ladderUser),
|
||||
currentStore: app.globalData.currentStore,
|
||||
});
|
||||
}
|
||||
@ -80,7 +98,7 @@ Page({
|
||||
|
||||
this.setData({
|
||||
userInfo: app.globalData.userInfo,
|
||||
ladderUser: app.globalData.ladderUser,
|
||||
ladderUser: this.normalizeLadderUser(app.globalData.ladderUser),
|
||||
currentStore: app.globalData.currentStore,
|
||||
});
|
||||
} catch (e) {
|
||||
@ -95,16 +113,35 @@ Page({
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
wx.showLoading({ title: "登录中..." });
|
||||
|
||||
try {
|
||||
// 如果没有微信登录信息,先登录
|
||||
if (!app.globalData.wxLoginInfo) {
|
||||
await app.wxLogin();
|
||||
}
|
||||
|
||||
// 手机号登录(先不传头像昵称)
|
||||
await app.phoneLogin(e.detail.encryptedData, e.detail.iv, null);
|
||||
const needGender =
|
||||
app.globalData.wxLoginInfo && app.globalData.wxLoginInfo.isNewUser;
|
||||
|
||||
if (
|
||||
needGender &&
|
||||
!(this.data.registerGender === 1 || this.data.registerGender === 2)
|
||||
) {
|
||||
this._pendingPhoneLogin = {
|
||||
encryptedData: e.detail.encryptedData,
|
||||
iv: e.detail.iv,
|
||||
};
|
||||
wx.hideLoading();
|
||||
this.setData({ showGenderModal: true });
|
||||
return;
|
||||
}
|
||||
|
||||
await this.doPhoneLogin(
|
||||
e.detail.encryptedData,
|
||||
e.detail.iv,
|
||||
needGender ? this.data.registerGender : 0,
|
||||
);
|
||||
|
||||
// 获取门店信息
|
||||
await app.getCurrentStore();
|
||||
@ -113,12 +150,10 @@ Page({
|
||||
|
||||
this.setData({
|
||||
userInfo: userInfo,
|
||||
ladderUser: app.globalData.ladderUser,
|
||||
ladderUser: this.normalizeLadderUser(app.globalData.ladderUser),
|
||||
currentStore: app.globalData.currentStore,
|
||||
});
|
||||
|
||||
wx.hideLoading();
|
||||
|
||||
// 检查是否需要完善资料(没有头像或昵称为默认值)
|
||||
const needProfile =
|
||||
!userInfo.avatar ||
|
||||
@ -143,12 +178,89 @@ Page({
|
||||
wx.showToast({ title: "登录成功", icon: "success" });
|
||||
}
|
||||
} catch (e) {
|
||||
wx.hideLoading();
|
||||
console.error("登录失败:", e);
|
||||
wx.showToast({ title: e.message || "登录失败", icon: "none" });
|
||||
} finally {
|
||||
wx.hideLoading();
|
||||
}
|
||||
},
|
||||
|
||||
async doPhoneLogin(encryptedData, iv, gender) {
|
||||
const g = gender === 1 || gender === 2 ? gender : 0;
|
||||
await app.phoneLogin(encryptedData, iv, g ? { gender: g } : null);
|
||||
this._pendingPhoneLogin = null;
|
||||
this.setData({ showGenderModal: false });
|
||||
},
|
||||
|
||||
onSelectRegisterGender(e) {
|
||||
const gender = Number(e.currentTarget.dataset.gender);
|
||||
if (gender !== 1 && gender !== 2) return;
|
||||
this.setData({ registerGender: gender });
|
||||
},
|
||||
|
||||
async onConfirmRegisterGender() {
|
||||
if (!(this.data.registerGender === 1 || this.data.registerGender === 2)) {
|
||||
wx.showToast({ title: "请选择性别", icon: "none" });
|
||||
return;
|
||||
}
|
||||
|
||||
const pending = this._pendingPhoneLogin;
|
||||
if (!pending) {
|
||||
this.setData({ showGenderModal: false });
|
||||
return;
|
||||
}
|
||||
|
||||
wx.showLoading({ title: "登录中..." });
|
||||
try {
|
||||
await this.doPhoneLogin(
|
||||
pending.encryptedData,
|
||||
pending.iv,
|
||||
this.data.registerGender,
|
||||
);
|
||||
|
||||
await app.getCurrentStore();
|
||||
const userInfo = app.globalData.userInfo;
|
||||
this.setData({
|
||||
userInfo: userInfo,
|
||||
ladderUser: this.normalizeLadderUser(app.globalData.ladderUser),
|
||||
currentStore: app.globalData.currentStore,
|
||||
});
|
||||
|
||||
const needProfile =
|
||||
!userInfo.avatar ||
|
||||
userInfo.avatar === "" ||
|
||||
!userInfo.nickname ||
|
||||
userInfo.nickname === "新用户" ||
|
||||
userInfo.nickname === "";
|
||||
|
||||
if (needProfile) {
|
||||
this.setData({
|
||||
showProfileModal: true,
|
||||
isEditMode: false,
|
||||
profileForm: {
|
||||
avatar: userInfo.avatar || "/images/avatar-default.svg",
|
||||
nickname:
|
||||
userInfo.nickname === "新用户" ? "" : userInfo.nickname || "",
|
||||
gender: userInfo.gender || 0,
|
||||
},
|
||||
});
|
||||
wx.showToast({ title: "登录成功,请完善资料", icon: "none" });
|
||||
} else {
|
||||
wx.showToast({ title: "登录成功", icon: "success" });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("登录失败:", e);
|
||||
wx.showToast({ title: e.message || "登录失败", icon: "none" });
|
||||
} finally {
|
||||
wx.hideLoading();
|
||||
}
|
||||
},
|
||||
|
||||
onCancelRegisterGender() {
|
||||
this._pendingPhoneLogin = null;
|
||||
this.setData({ showGenderModal: false, registerGender: 0 });
|
||||
},
|
||||
|
||||
// 点击头像,打开编辑资料弹框
|
||||
onTapAvatar() {
|
||||
if (!this.data.userInfo || !this.data.userInfo.phone) return;
|
||||
@ -159,6 +271,7 @@ Page({
|
||||
profileForm: {
|
||||
avatar: this.data.userInfo.avatar || "/images/avatar-default.svg",
|
||||
nickname: this.data.userInfo.nickname || "",
|
||||
gender: this.data.userInfo.gender || 0,
|
||||
},
|
||||
});
|
||||
},
|
||||
@ -178,9 +291,17 @@ Page({
|
||||
});
|
||||
},
|
||||
|
||||
onProfileGenderSelect(e) {
|
||||
const gender = Number(e.currentTarget.dataset.gender);
|
||||
if (gender !== 1 && gender !== 2) return;
|
||||
this.setData({
|
||||
"profileForm.gender": gender,
|
||||
});
|
||||
},
|
||||
|
||||
// 确认保存资料
|
||||
async saveProfile() {
|
||||
const { avatar, nickname } = this.data.profileForm;
|
||||
const { avatar, nickname, gender } = this.data.profileForm;
|
||||
|
||||
if (!nickname || nickname.trim() === "") {
|
||||
wx.showToast({ title: "请输入昵称", icon: "none" });
|
||||
@ -200,26 +321,28 @@ Page({
|
||||
}
|
||||
|
||||
// 调用更新资料接口
|
||||
const res = await app.request(
|
||||
"/api/user/profile",
|
||||
{
|
||||
const payload = {
|
||||
nickname: nickname.trim(),
|
||||
avatar: avatarUrl,
|
||||
},
|
||||
"PUT",
|
||||
);
|
||||
};
|
||||
if (gender === 1 || gender === 2) {
|
||||
payload.gender = gender;
|
||||
}
|
||||
|
||||
const res = await app.request("/api/user/profile", payload, "PUT");
|
||||
|
||||
// 更新本地数据(服务端已返回完整URL)
|
||||
const userInfo = Object.assign({}, this.data.userInfo, {
|
||||
nickname: (res.data && res.data.nickname) || nickname.trim(),
|
||||
avatar: (res.data && res.data.avatar) || avatarUrl,
|
||||
gender: (res.data && res.data.gender) || this.data.userInfo.gender || 0,
|
||||
});
|
||||
app.globalData.userInfo = userInfo;
|
||||
|
||||
this.setData({
|
||||
userInfo: userInfo,
|
||||
showProfileModal: false,
|
||||
profileForm: { avatar: "", nickname: "" },
|
||||
profileForm: { avatar: "", nickname: "", gender: 0 },
|
||||
});
|
||||
|
||||
wx.hideLoading();
|
||||
|
||||
@ -101,15 +101,11 @@
|
||||
<text class="record-label">胜场</text>
|
||||
</view>
|
||||
<view class="record-item">
|
||||
<text class="record-value">
|
||||
{{(ladderUser.matchCount || 0) - (ladderUser.winCount || 0)}}
|
||||
</text>
|
||||
<text class="record-value">{{ladderUser.loseCount || 0}}</text>
|
||||
<text class="record-label">负场</text>
|
||||
</view>
|
||||
<view class="record-item">
|
||||
<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-value rate">{{ladderUser.winRate || 0}}%</text>
|
||||
<text class="record-label">胜率</text>
|
||||
</view>
|
||||
</view>
|
||||
@ -155,6 +151,34 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 注册性别选择弹窗(新用户必填) -->
|
||||
<view class="gender-overlay {{showGenderModal ? 'show' : ''}}" bindtap="onCancelRegisterGender">
|
||||
<view class="gender-modal {{showGenderModal ? 'show' : ''}}" catchtap="preventBubble">
|
||||
<view class="gender-modal-header">
|
||||
<text class="gender-modal-title">请选择性别</text>
|
||||
<view class="gender-modal-close" bindtap="onCancelRegisterGender">×</view>
|
||||
</view>
|
||||
<view class="gender-modal-body">
|
||||
<view class="gender-tip">
|
||||
<text>新用户注册需填写性别</text>
|
||||
</view>
|
||||
<view class="gender-options">
|
||||
<view class="gender-option {{registerGender === 1 ? 'active' : ''}}" bindtap="onSelectRegisterGender" data-gender="1">
|
||||
<text class="gender-emoji">👦</text>
|
||||
<text class="gender-text">男</text>
|
||||
</view>
|
||||
<view class="gender-option {{registerGender === 2 ? 'active' : ''}}" bindtap="onSelectRegisterGender" data-gender="2">
|
||||
<text class="gender-emoji">👧</text>
|
||||
<text class="gender-text">女</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="gender-modal-footer">
|
||||
<button class="gender-btn-cancel" bindtap="onCancelRegisterGender">取消</button>
|
||||
<button class="gender-btn-confirm" bindtap="onConfirmRegisterGender">确定</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 会员码弹窗 -->
|
||||
<view class="qrcode-overlay {{showQrcode ? 'show' : ''}}" bindtap="hideQrcode">
|
||||
<view class="qrcode-modal {{showQrcode ? 'show' : ''}}" catchtap="preventBubble">
|
||||
@ -220,6 +244,18 @@
|
||||
<text class="profile-label">昵称</text>
|
||||
<input class="nickname-input" type="nickname" placeholder="请输入昵称" value="{{profileForm.nickname}}" bindinput="onNicknameInput" maxlength="20" />
|
||||
</view>
|
||||
<!-- 性别选择 -->
|
||||
<view class="profile-gender-section">
|
||||
<text class="profile-label">性别</text>
|
||||
<view class="gender-options-inline">
|
||||
<view class="gender-chip {{profileForm.gender === 1 ? 'active' : ''}}" bindtap="onProfileGenderSelect" data-gender="1">
|
||||
男
|
||||
</view>
|
||||
<view class="gender-chip {{profileForm.gender === 2 ? 'active' : ''}}" bindtap="onProfileGenderSelect" data-gender="2">
|
||||
女
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="profile-modal-footer">
|
||||
<button class="profile-btn-cancel" bindtap="closeProfileModal">
|
||||
|
||||
@ -458,19 +458,20 @@
|
||||
}
|
||||
|
||||
.ladder-record {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
align-items: center;
|
||||
padding-top: 20rpx;
|
||||
border-top: 1rpx solid var(--border-soft);
|
||||
}
|
||||
|
||||
.record-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.record-value {
|
||||
@ -479,6 +480,7 @@
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 4rpx;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.record-value.win {
|
||||
@ -492,6 +494,7 @@
|
||||
.record-label {
|
||||
font-size: 20rpx;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
/* ==========================================
|
||||
@ -862,6 +865,174 @@
|
||||
transition: all 0.35s ease;
|
||||
}
|
||||
|
||||
/* ==========================================
|
||||
注册性别选择弹窗
|
||||
========================================== */
|
||||
.gender-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
visibility: hidden;
|
||||
transition: all 0.35s ease;
|
||||
}
|
||||
|
||||
.gender-overlay.show {
|
||||
visibility: visible;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
|
||||
.gender-modal {
|
||||
width: 88%;
|
||||
max-width: 640rpx;
|
||||
background: var(--bg-white);
|
||||
border-radius: var(--radius-xl);
|
||||
overflow: hidden;
|
||||
transform: scale(0.85) translateY(40rpx);
|
||||
opacity: 0;
|
||||
transition: all 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
box-shadow: 0 24rpx 80rpx rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.gender-modal.show {
|
||||
transform: scale(1) translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.gender-modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 28rpx 32rpx;
|
||||
background: linear-gradient(90deg, #fff8f5, var(--bg-white));
|
||||
border-bottom: 1rpx solid var(--border-soft);
|
||||
}
|
||||
|
||||
.gender-modal-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.gender-modal-close {
|
||||
width: 52rpx;
|
||||
height: 52rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 36rpx;
|
||||
color: var(--text-muted);
|
||||
background: var(--bg-soft);
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.gender-modal-close:active {
|
||||
background: var(--border-light);
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.gender-modal-body {
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
.gender-tip {
|
||||
padding: 18rpx 22rpx;
|
||||
background: #fffbf5;
|
||||
border: 1rpx solid #ffe8d5;
|
||||
border-radius: var(--radius-lg);
|
||||
margin-bottom: 26rpx;
|
||||
color: #b7791f;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.gender-options {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 18rpx;
|
||||
}
|
||||
|
||||
.gender-option {
|
||||
height: 120rpx;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 2rpx solid var(--border-light);
|
||||
background: var(--bg-soft);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10rpx;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.gender-option:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.gender-option.active {
|
||||
border-color: var(--primary);
|
||||
background: #fff8f5;
|
||||
box-shadow: 0 10rpx 28rpx rgba(255, 107, 53, 0.12);
|
||||
}
|
||||
|
||||
.gender-emoji {
|
||||
font-size: 42rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.gender-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.gender-modal-footer {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
padding: 24rpx 32rpx 32rpx;
|
||||
background: var(--bg-white);
|
||||
}
|
||||
|
||||
.gender-btn-cancel,
|
||||
.gender-btn-confirm {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--radius-lg);
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.gender-btn-cancel {
|
||||
background: var(--bg-soft);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.gender-btn-cancel:active {
|
||||
background: var(--border-light);
|
||||
}
|
||||
|
||||
.gender-btn-confirm {
|
||||
background: var(--primary-gradient);
|
||||
color: #fff;
|
||||
box-shadow: var(--shadow-primary);
|
||||
}
|
||||
|
||||
.gender-btn-confirm:active {
|
||||
transform: scale(0.96);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.profile-overlay.show {
|
||||
visibility: visible;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
@ -1019,6 +1190,43 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.profile-gender-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 28rpx;
|
||||
}
|
||||
|
||||
.gender-options-inline {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.gender-chip {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--bg-soft);
|
||||
border: 2rpx solid var(--border-light);
|
||||
color: var(--text-secondary);
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.gender-chip:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.gender-chip.active {
|
||||
background: var(--primary-gradient);
|
||||
border-color: rgba(255, 107, 53, 0.25);
|
||||
color: #fff;
|
||||
box-shadow: var(--shadow-primary);
|
||||
}
|
||||
|
||||
.nickname-input {
|
||||
height: 96rpx;
|
||||
padding: 0 24rpx;
|
||||
|
||||
@ -7,7 +7,8 @@
|
||||
"start": "node src/app.js",
|
||||
"dev": "nodemon src/app.js",
|
||||
"db:init": "node src/scripts/initDatabase.js",
|
||||
"db:mock": "node src/scripts/generateMockLadderData.js"
|
||||
"db:mock": "node src/scripts/generateMockLadderData.js",
|
||||
"db:challenge-weight-1": "node src/scripts/setChallengeMatchWeightTo1.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
|
||||
@ -75,7 +75,7 @@ const POWER_CALC = {
|
||||
// 比赛权重
|
||||
const MATCH_WEIGHTS = {
|
||||
DAILY: 1.0, // 日常畅打
|
||||
CHALLENGE: 1.5, // 挑战赛
|
||||
CHALLENGE: 1.0, // 挑战赛
|
||||
MONTHLY: 1.5, // 月度排位赛
|
||||
SEASONAL: 2.0, // 季度/年度总决赛
|
||||
};
|
||||
|
||||
@ -24,6 +24,7 @@ class LadderController {
|
||||
page = 1,
|
||||
pageSize = 50,
|
||||
is_display,
|
||||
no_count,
|
||||
} = req.query;
|
||||
const { limit, offset } = getPagination(page, pageSize);
|
||||
|
||||
@ -50,15 +51,39 @@ class LadderController {
|
||||
where.level = level;
|
||||
}
|
||||
|
||||
const { rows, count } = await LadderUser.findAndCountAll({
|
||||
where,
|
||||
include: [
|
||||
const include = [
|
||||
{ model: User, as: "user", attributes: ["nickname", "avatar"] },
|
||||
],
|
||||
order: [["power_score", "DESC"]],
|
||||
];
|
||||
const order = [["power_score", "DESC"]];
|
||||
|
||||
const useNoCount =
|
||||
no_count === "1" ||
|
||||
no_count === 1 ||
|
||||
no_count === true ||
|
||||
no_count === "true";
|
||||
|
||||
let rows = [];
|
||||
let count = 0;
|
||||
if (useNoCount) {
|
||||
rows = await LadderUser.findAll({
|
||||
where,
|
||||
include,
|
||||
order,
|
||||
limit,
|
||||
offset,
|
||||
});
|
||||
count = offset === 0 ? rows.length : offset + rows.length;
|
||||
} else {
|
||||
const result = await LadderUser.findAndCountAll({
|
||||
where,
|
||||
include,
|
||||
order,
|
||||
limit,
|
||||
offset,
|
||||
});
|
||||
rows = result.rows;
|
||||
count = result.count;
|
||||
}
|
||||
|
||||
// 添加排名
|
||||
const startRank = offset + 1;
|
||||
@ -88,6 +113,99 @@ class LadderController {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取我的排名(用于小程序定位)
|
||||
async getMyRank(req, res) {
|
||||
try {
|
||||
const user = req.user;
|
||||
const { store_id, gender, pageSize = 20, is_display } = req.query;
|
||||
|
||||
if (!store_id) {
|
||||
return res.status(400).json(error("缺少门店ID", 400));
|
||||
}
|
||||
|
||||
const normalizedGender =
|
||||
gender === undefined || gender === null || gender === ""
|
||||
? null
|
||||
: Number(gender);
|
||||
if (
|
||||
normalizedGender !== null &&
|
||||
normalizedGender !== 1 &&
|
||||
normalizedGender !== 2
|
||||
) {
|
||||
return res.status(400).json(error("性别参数无效", 400));
|
||||
}
|
||||
|
||||
const normalizedPageSize = Math.min(parseInt(pageSize) || 20, 100);
|
||||
|
||||
const ladderUser = await LadderUser.findOne({
|
||||
where: {
|
||||
store_id,
|
||||
user_id: user.id,
|
||||
status: 1,
|
||||
},
|
||||
});
|
||||
|
||||
if (!ladderUser) {
|
||||
return res.status(404).json(error("未加入该门店天梯", 404));
|
||||
}
|
||||
|
||||
if (
|
||||
!is_display &&
|
||||
(ladderUser.monthly_match_count || 0) < POWER_CALC.MIN_MONTHLY_MATCHES
|
||||
) {
|
||||
return res.json(
|
||||
success({
|
||||
qualified: false,
|
||||
ladderUserId: ladderUser.id,
|
||||
monthlyMatchCount: ladderUser.monthly_match_count || 0,
|
||||
minMonthlyMatches: POWER_CALC.MIN_MONTHLY_MATCHES,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const where = {
|
||||
store_id,
|
||||
status: 1,
|
||||
};
|
||||
|
||||
if (!is_display) {
|
||||
where.monthly_match_count = {
|
||||
[Op.gte]: POWER_CALC.MIN_MONTHLY_MATCHES,
|
||||
};
|
||||
}
|
||||
|
||||
if (normalizedGender) {
|
||||
where.gender = normalizedGender;
|
||||
}
|
||||
|
||||
const higherCount = await LadderUser.count({
|
||||
where: {
|
||||
...where,
|
||||
power_score: { [Op.gt]: ladderUser.power_score },
|
||||
},
|
||||
});
|
||||
|
||||
const rank = higherCount + 1;
|
||||
const page = Math.floor((rank - 1) / normalizedPageSize) + 1;
|
||||
const indexInPage = (rank - 1) % normalizedPageSize;
|
||||
|
||||
res.json(
|
||||
success({
|
||||
qualified: true,
|
||||
ladderUserId: ladderUser.id,
|
||||
rank,
|
||||
page,
|
||||
indexInPage,
|
||||
powerScore: ladderUser.power_score,
|
||||
gender: ladderUser.gender,
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("获取我的排名失败:", err);
|
||||
res.status(500).json(error("获取失败"));
|
||||
}
|
||||
}
|
||||
|
||||
// 获取天梯用户详情
|
||||
async getUserDetail(req, res) {
|
||||
try {
|
||||
|
||||
@ -186,7 +186,23 @@ class MatchAdminController {
|
||||
return res.status(404).json(error('比赛不存在', 404));
|
||||
}
|
||||
|
||||
await match.update({ name, weight, referee_id });
|
||||
const updateData = {};
|
||||
if (name !== undefined) updateData.name = name;
|
||||
if (referee_id !== undefined) updateData.referee_id = referee_id;
|
||||
|
||||
if (weight !== undefined) {
|
||||
const normalizedWeight = parseFloat(weight);
|
||||
if (!Number.isFinite(normalizedWeight) || normalizedWeight <= 0) {
|
||||
return res.status(400).json(error('权重参数无效', 400));
|
||||
}
|
||||
updateData.weight = normalizedWeight;
|
||||
}
|
||||
|
||||
if (match.type === MATCH_TYPES.CHALLENGE) {
|
||||
updateData.weight = 1.0;
|
||||
}
|
||||
|
||||
await match.update(updateData);
|
||||
res.json(success(null, '更新成功'));
|
||||
} catch (err) {
|
||||
console.error('更新比赛失败:', err);
|
||||
|
||||
@ -1310,6 +1310,70 @@ class MatchController {
|
||||
}
|
||||
}
|
||||
|
||||
// 大屏:进行中/近7天比赛列表(公开)
|
||||
async getDisplayList(req, res) {
|
||||
try {
|
||||
const { store_id, days = 7, limit = 20 } = req.query;
|
||||
|
||||
const normalizedDays = Math.min(Math.max(parseInt(days) || 7, 1), 30);
|
||||
const normalizedLimit = Math.min(Math.max(parseInt(limit) || 20, 1), 100);
|
||||
const sinceTime = new Date(Date.now() - normalizedDays * 24 * 60 * 60 * 1000);
|
||||
|
||||
const where = {
|
||||
status: { [Op.in]: [MATCH_STATUS.PENDING, MATCH_STATUS.ONGOING, MATCH_STATUS.FINISHED] },
|
||||
[Op.or]: [
|
||||
{ status: { [Op.in]: [MATCH_STATUS.PENDING, MATCH_STATUS.ONGOING] } },
|
||||
{ created_at: { [Op.gte]: sinceTime } },
|
||||
{ start_time: { [Op.gte]: sinceTime } },
|
||||
],
|
||||
};
|
||||
if (store_id) where.store_id = store_id;
|
||||
|
||||
const matches = await Match.findAll({
|
||||
where,
|
||||
include: [{ model: Store, as: "store", attributes: ["id", "name"] }],
|
||||
order: [
|
||||
["status", "ASC"],
|
||||
["start_time", "DESC"],
|
||||
["created_at", "DESC"],
|
||||
],
|
||||
limit: normalizedLimit,
|
||||
});
|
||||
|
||||
const list = matches.map((match) => ({
|
||||
id: match.id,
|
||||
matchCode: match.match_code,
|
||||
type: match.type,
|
||||
typeName: match.type === MATCH_TYPES.CHALLENGE ? "挑战赛" : "排位赛",
|
||||
name: match.name,
|
||||
status: match.status,
|
||||
statusName:
|
||||
match.status === MATCH_STATUS.PENDING
|
||||
? "待开始"
|
||||
: match.status === MATCH_STATUS.ONGOING
|
||||
? "进行中"
|
||||
: match.status === MATCH_STATUS.FINISHED
|
||||
? "已结束"
|
||||
: "已取消",
|
||||
stage: match.stage,
|
||||
stageName:
|
||||
match.type === MATCH_TYPES.RANKING
|
||||
? ["报名中", "循环赛", "淘汰赛", "已结束"][match.stage]
|
||||
: null,
|
||||
startTime: match.start_time,
|
||||
endTime: match.end_time,
|
||||
createdAt: match.created_at,
|
||||
storeId: match.store?.id,
|
||||
storeName: match.store?.name,
|
||||
}));
|
||||
|
||||
res.json(success(list));
|
||||
} catch (err) {
|
||||
console.error("获取大屏比赛列表失败:", err);
|
||||
res.status(500).json(error("获取失败"));
|
||||
}
|
||||
}
|
||||
|
||||
// 获取待确认的比赛
|
||||
async getPendingConfirm(req, res) {
|
||||
try {
|
||||
|
||||
@ -1,9 +1,21 @@
|
||||
const { User, PointAction, PointRecord, PointProduct, PointOrder, Store } = require('../models');
|
||||
const { ORDER_STATUS, POINTS_CALC } = require('../config/constants');
|
||||
const { success, error, getPagination, pageResult } = require('../utils/helper');
|
||||
const PowerCalculator = require('../services/powerCalculator');
|
||||
const { Op } = require('sequelize');
|
||||
const sequelize = require('../config/database');
|
||||
const {
|
||||
User,
|
||||
PointAction,
|
||||
PointRecord,
|
||||
PointProduct,
|
||||
PointOrder,
|
||||
Store,
|
||||
} = require("../models");
|
||||
const { ORDER_STATUS, POINTS_CALC } = require("../config/constants");
|
||||
const {
|
||||
success,
|
||||
error,
|
||||
getPagination,
|
||||
pageResult,
|
||||
} = require("../utils/helper");
|
||||
const PowerCalculator = require("../services/powerCalculator");
|
||||
const { Op } = require("sequelize");
|
||||
const sequelize = require("../config/database");
|
||||
|
||||
class PointsAdminController {
|
||||
// === 积分行为管理 ===
|
||||
@ -14,7 +26,7 @@ class PointsAdminController {
|
||||
const admin = req.admin;
|
||||
|
||||
const where = { status: 1 };
|
||||
if (admin.role === 'super_admin') {
|
||||
if (admin.role === "super_admin") {
|
||||
if (store_id) where.store_id = store_id;
|
||||
} else {
|
||||
where.store_id = admin.store_id;
|
||||
@ -22,13 +34,18 @@ class PointsAdminController {
|
||||
|
||||
const { rows, count } = await PointAction.findAndCountAll({
|
||||
where,
|
||||
include: [{ model: Store, as: 'store', attributes: ['id', 'name'] }],
|
||||
order: [['show_quick', 'DESC'], ['created_at', 'DESC']],
|
||||
include: [{ model: Store, as: "store", attributes: ["id", "name"] }],
|
||||
order: [
|
||||
["show_quick", "DESC"],
|
||||
["created_at", "DESC"],
|
||||
],
|
||||
limit,
|
||||
offset
|
||||
offset,
|
||||
});
|
||||
|
||||
res.json(pageResult(rows.map(action => ({
|
||||
res.json(
|
||||
pageResult(
|
||||
rows.map((action) => ({
|
||||
id: action.id,
|
||||
name: action.name,
|
||||
description: action.description,
|
||||
@ -36,22 +53,29 @@ class PointsAdminController {
|
||||
isConsume: action.is_consume,
|
||||
showQuick: action.show_quick,
|
||||
storeId: action.store_id,
|
||||
storeName: action.store?.name
|
||||
})), count, page, pageSize));
|
||||
storeName: action.store?.name,
|
||||
})),
|
||||
count,
|
||||
page,
|
||||
pageSize,
|
||||
),
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('获取积分行为列表失败:', err);
|
||||
res.status(500).json(error('获取失败'));
|
||||
console.error("获取积分行为列表失败:", err);
|
||||
res.status(500).json(error("获取失败"));
|
||||
}
|
||||
}
|
||||
|
||||
async createAction(req, res) {
|
||||
try {
|
||||
const { store_id, name, description, points, is_consume, show_quick } = req.body;
|
||||
const { store_id, name, description, points, is_consume, show_quick } =
|
||||
req.body;
|
||||
const admin = req.admin;
|
||||
|
||||
const targetStoreId = admin.role === 'super_admin' ? store_id : admin.store_id;
|
||||
const targetStoreId =
|
||||
admin.role === "super_admin" ? store_id : admin.store_id;
|
||||
if (!targetStoreId) {
|
||||
return res.status(400).json(error('请选择门店', 400));
|
||||
return res.status(400).json(error("请选择门店", 400));
|
||||
}
|
||||
|
||||
const action = await PointAction.create({
|
||||
@ -61,24 +85,25 @@ class PointsAdminController {
|
||||
points: points || 0,
|
||||
is_consume: is_consume || 0,
|
||||
show_quick: show_quick || 0,
|
||||
status: 1
|
||||
status: 1,
|
||||
});
|
||||
|
||||
res.json(success({ id: action.id }, '创建成功'));
|
||||
res.json(success({ id: action.id }, "创建成功"));
|
||||
} catch (err) {
|
||||
console.error('创建积分行为失败:', err);
|
||||
res.status(500).json(error('创建失败'));
|
||||
console.error("创建积分行为失败:", err);
|
||||
res.status(500).json(error("创建失败"));
|
||||
}
|
||||
}
|
||||
|
||||
async updateAction(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { name, description, points, is_consume, show_quick, status } = req.body;
|
||||
const { name, description, points, is_consume, show_quick, status } =
|
||||
req.body;
|
||||
|
||||
const action = await PointAction.findByPk(id);
|
||||
if (!action) {
|
||||
return res.status(404).json(error('行为不存在', 404));
|
||||
return res.status(404).json(error("行为不存在", 404));
|
||||
}
|
||||
|
||||
await action.update({
|
||||
@ -87,13 +112,13 @@ class PointsAdminController {
|
||||
points: points !== undefined ? points : action.points,
|
||||
is_consume: is_consume !== undefined ? is_consume : action.is_consume,
|
||||
show_quick: show_quick !== undefined ? show_quick : action.show_quick,
|
||||
status: status !== undefined ? status : action.status
|
||||
status: status !== undefined ? status : action.status,
|
||||
});
|
||||
|
||||
res.json(success(null, '更新成功'));
|
||||
res.json(success(null, "更新成功"));
|
||||
} catch (err) {
|
||||
console.error('更新积分行为失败:', err);
|
||||
res.status(500).json(error('更新失败'));
|
||||
console.error("更新积分行为失败:", err);
|
||||
res.status(500).json(error("更新失败"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,14 +128,14 @@ class PointsAdminController {
|
||||
|
||||
const action = await PointAction.findByPk(id);
|
||||
if (!action) {
|
||||
return res.status(404).json(error('行为不存在', 404));
|
||||
return res.status(404).json(error("行为不存在", 404));
|
||||
}
|
||||
|
||||
await action.update({ status: 0 });
|
||||
res.json(success(null, '删除成功'));
|
||||
res.json(success(null, "删除成功"));
|
||||
} catch (err) {
|
||||
console.error('删除积分行为失败:', err);
|
||||
res.status(500).json(error('删除失败'));
|
||||
console.error("删除积分行为失败:", err);
|
||||
res.status(500).json(error("删除失败"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -121,52 +146,95 @@ class PointsAdminController {
|
||||
const { action_id, member_code, consume_amount } = req.body;
|
||||
const admin = req.admin;
|
||||
|
||||
if (!action_id) {
|
||||
await t.rollback();
|
||||
return res.status(400).json(error("缺少行为ID", 400));
|
||||
}
|
||||
if (!member_code) {
|
||||
await t.rollback();
|
||||
return res.status(400).json(error("缺少用户信息", 400));
|
||||
}
|
||||
|
||||
// 获取行为
|
||||
const action = await PointAction.findByPk(action_id);
|
||||
if (!action || action.status !== 1) {
|
||||
await t.rollback();
|
||||
return res.status(404).json(error('行为不存在', 404));
|
||||
return res.status(404).json(error("行为不存在", 404));
|
||||
}
|
||||
|
||||
if (
|
||||
admin.role !== "super_admin" &&
|
||||
String(action.store_id) !== String(admin.store_id)
|
||||
) {
|
||||
await t.rollback();
|
||||
return res.status(403).json(error("无权限操作该门店行为", 403));
|
||||
}
|
||||
|
||||
// 通过会员码、手机号或昵称获取用户
|
||||
let user = null;
|
||||
if (member_code) {
|
||||
const keyword = String(member_code).trim();
|
||||
// 先尝试通过会员码查询
|
||||
user = await User.findOne({ where: { member_code } });
|
||||
user = await User.findOne({
|
||||
where: { member_code: keyword, status: 1 },
|
||||
});
|
||||
|
||||
// 如果会员码查询失败,尝试通过手机号查询
|
||||
if (!user && /^1[3-9]\d{9}$/.test(member_code)) {
|
||||
user = await User.findOne({ where: { phone: member_code } });
|
||||
if (!user && /^1[3-9]\d{9}$/.test(keyword)) {
|
||||
user = await User.findOne({ where: { phone: keyword, status: 1 } });
|
||||
}
|
||||
image.png
|
||||
// 如果还是没找到,尝试通过昵称查询
|
||||
if (!user) {
|
||||
user = await User.findOne({
|
||||
where: {
|
||||
nickname: { [Op.like]: `%${member_code}%` },
|
||||
status: 1
|
||||
}
|
||||
nickname: { [Op.like]: `%${keyword}%` },
|
||||
status: 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
await t.rollback();
|
||||
return res.status(404).json(error('用户不存在', 404));
|
||||
return res.status(404).json(error("用户不存在", 404));
|
||||
}
|
||||
|
||||
// 计算积分
|
||||
let points = action.points;
|
||||
if (action.is_consume === 1 && consume_amount) {
|
||||
points = PowerCalculator.calculateConsumePoints(parseFloat(consume_amount));
|
||||
let points = Number(action.points);
|
||||
if (!Number.isFinite(points)) {
|
||||
await t.rollback();
|
||||
return res.status(400).json(error("行为积分配置无效", 400));
|
||||
}
|
||||
if (
|
||||
action.is_consume === 1 &&
|
||||
consume_amount !== undefined &&
|
||||
consume_amount !== null &&
|
||||
consume_amount !== ""
|
||||
) {
|
||||
const amount = Number(consume_amount);
|
||||
if (!Number.isFinite(amount) || amount < 0) {
|
||||
await t.rollback();
|
||||
return res.status(400).json(error("消费金额无效", 400));
|
||||
}
|
||||
points = PowerCalculator.calculateConsumePoints(amount);
|
||||
}
|
||||
|
||||
// 更新用户积分
|
||||
const newBalance = user.total_points + points;
|
||||
const currentBalance = Number(user.total_points) || 0;
|
||||
const newBalance = currentBalance + points;
|
||||
if (!Number.isFinite(newBalance)) {
|
||||
await t.rollback();
|
||||
return res.status(400).json(error("积分计算失败", 400));
|
||||
}
|
||||
if (newBalance < 0) {
|
||||
await t.rollback();
|
||||
return res.status(400).json(error("积分不足", 400));
|
||||
}
|
||||
await user.update({ total_points: newBalance }, { transaction: t });
|
||||
|
||||
// 记录积分变动
|
||||
await PointRecord.create({
|
||||
await PointRecord.create(
|
||||
{
|
||||
user_id: user.id,
|
||||
store_id: action.store_id,
|
||||
action_id: action.id,
|
||||
@ -174,21 +242,28 @@ class PointsAdminController {
|
||||
points,
|
||||
balance: newBalance,
|
||||
consume_amount: action.is_consume === 1 ? consume_amount : null,
|
||||
operator_id: admin.id
|
||||
}, { transaction: t });
|
||||
operator_id: admin.id,
|
||||
},
|
||||
{ transaction: t },
|
||||
);
|
||||
|
||||
await t.commit();
|
||||
|
||||
res.json(success({
|
||||
res.json(
|
||||
success(
|
||||
{
|
||||
userId: user.id,
|
||||
nickname: user.nickname,
|
||||
points,
|
||||
newBalance
|
||||
}, '操作成功'));
|
||||
newBalance,
|
||||
},
|
||||
"操作成功",
|
||||
),
|
||||
);
|
||||
} catch (err) {
|
||||
await t.rollback();
|
||||
console.error('执行积分行为失败:', err);
|
||||
res.status(500).json(error('操作失败'));
|
||||
console.error("执行积分行为失败:", err);
|
||||
res.status(500).json(error("操作失败"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -200,24 +275,29 @@ class PointsAdminController {
|
||||
const admin = req.admin;
|
||||
|
||||
const where = {};
|
||||
if (admin.role === 'super_admin') {
|
||||
if (admin.role === "super_admin") {
|
||||
if (store_id) where.store_id = store_id;
|
||||
} else {
|
||||
where.store_id = admin.store_id;
|
||||
}
|
||||
if (status !== undefined && status !== '') {
|
||||
if (status !== undefined && status !== "") {
|
||||
where.status = status;
|
||||
}
|
||||
|
||||
const { rows, count } = await PointProduct.findAndCountAll({
|
||||
where,
|
||||
include: [{ model: Store, as: 'store', attributes: ['id', 'name'] }],
|
||||
order: [['sort_order', 'ASC'], ['created_at', 'DESC']],
|
||||
include: [{ model: Store, as: "store", attributes: ["id", "name"] }],
|
||||
order: [
|
||||
["sort_order", "ASC"],
|
||||
["created_at", "DESC"],
|
||||
],
|
||||
limit,
|
||||
offset
|
||||
offset,
|
||||
});
|
||||
|
||||
res.json(pageResult(rows.map(product => ({
|
||||
res.json(
|
||||
pageResult(
|
||||
rows.map((product) => ({
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
description: product.description,
|
||||
@ -230,22 +310,37 @@ class PointsAdminController {
|
||||
status: product.status,
|
||||
storeId: product.store_id,
|
||||
storeName: product.store?.name,
|
||||
createdAt: product.created_at
|
||||
})), count, page, pageSize));
|
||||
createdAt: product.created_at,
|
||||
})),
|
||||
count,
|
||||
page,
|
||||
pageSize,
|
||||
),
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('获取商品列表失败:', err);
|
||||
res.status(500).json(error('获取失败'));
|
||||
console.error("获取商品列表失败:", err);
|
||||
res.status(500).json(error("获取失败"));
|
||||
}
|
||||
}
|
||||
|
||||
async createProduct(req, res) {
|
||||
try {
|
||||
const { store_id, name, description, image, points_required, original_price, stock, sort_order } = req.body;
|
||||
const {
|
||||
store_id,
|
||||
name,
|
||||
description,
|
||||
image,
|
||||
points_required,
|
||||
original_price,
|
||||
stock,
|
||||
sort_order,
|
||||
} = req.body;
|
||||
const admin = req.admin;
|
||||
|
||||
const targetStoreId = admin.role === 'super_admin' ? store_id : admin.store_id;
|
||||
const targetStoreId =
|
||||
admin.role === "super_admin" ? store_id : admin.store_id;
|
||||
if (!targetStoreId) {
|
||||
return res.status(400).json(error('请选择门店', 400));
|
||||
return res.status(400).json(error("请选择门店", 400));
|
||||
}
|
||||
|
||||
const product = await PointProduct.create({
|
||||
@ -257,13 +352,13 @@ class PointsAdminController {
|
||||
original_price,
|
||||
stock: stock || 0,
|
||||
sort_order: sort_order || 0,
|
||||
status: 1
|
||||
status: 1,
|
||||
});
|
||||
|
||||
res.json(success({ id: product.id }, '创建成功'));
|
||||
res.json(success({ id: product.id }, "创建成功"));
|
||||
} catch (err) {
|
||||
console.error('创建商品失败:', err);
|
||||
res.status(500).json(error('创建失败'));
|
||||
console.error("创建商品失败:", err);
|
||||
res.status(500).json(error("创建失败"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -274,14 +369,14 @@ class PointsAdminController {
|
||||
|
||||
const product = await PointProduct.findByPk(id);
|
||||
if (!product) {
|
||||
return res.status(404).json(error('商品不存在', 404));
|
||||
return res.status(404).json(error("商品不存在", 404));
|
||||
}
|
||||
|
||||
await product.update(data);
|
||||
res.json(success(null, '更新成功'));
|
||||
res.json(success(null, "更新成功"));
|
||||
} catch (err) {
|
||||
console.error('更新商品失败:', err);
|
||||
res.status(500).json(error('更新失败'));
|
||||
console.error("更新商品失败:", err);
|
||||
res.status(500).json(error("更新失败"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -291,14 +386,14 @@ class PointsAdminController {
|
||||
|
||||
const product = await PointProduct.findByPk(id);
|
||||
if (!product) {
|
||||
return res.status(404).json(error('商品不存在', 404));
|
||||
return res.status(404).json(error("商品不存在", 404));
|
||||
}
|
||||
|
||||
await product.update({ status: 0 });
|
||||
res.json(success(null, '删除成功'));
|
||||
res.json(success(null, "删除成功"));
|
||||
} catch (err) {
|
||||
console.error('删除商品失败:', err);
|
||||
res.status(500).json(error('删除失败'));
|
||||
console.error("删除商品失败:", err);
|
||||
res.status(500).json(error("删除失败"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -310,34 +405,44 @@ class PointsAdminController {
|
||||
const admin = req.admin;
|
||||
|
||||
const where = {};
|
||||
if (admin.role === 'super_admin') {
|
||||
if (admin.role === "super_admin") {
|
||||
if (store_id) where.store_id = store_id;
|
||||
} else {
|
||||
where.store_id = admin.store_id;
|
||||
}
|
||||
if (status !== undefined && status !== '') {
|
||||
if (status !== undefined && status !== "") {
|
||||
where.status = status;
|
||||
}
|
||||
if (keyword) {
|
||||
where[Op.or] = [
|
||||
{ order_no: { [Op.like]: `%${keyword}%` } },
|
||||
{ exchange_code: { [Op.like]: `%${keyword}%` } }
|
||||
{ exchange_code: { [Op.like]: `%${keyword}%` } },
|
||||
];
|
||||
}
|
||||
|
||||
const { rows, count } = await PointOrder.findAndCountAll({
|
||||
where,
|
||||
include: [
|
||||
{ model: User, as: 'user', attributes: ['id', 'nickname', 'phone', 'member_code'] },
|
||||
{ model: PointProduct, as: 'product', attributes: ['id', 'name', 'image'] },
|
||||
{ model: Store, as: 'store', attributes: ['id', 'name'] }
|
||||
{
|
||||
model: User,
|
||||
as: "user",
|
||||
attributes: ["id", "nickname", "phone", "member_code"],
|
||||
},
|
||||
{
|
||||
model: PointProduct,
|
||||
as: "product",
|
||||
attributes: ["id", "name", "image"],
|
||||
},
|
||||
{ model: Store, as: "store", attributes: ["id", "name"] },
|
||||
],
|
||||
order: [['created_at', 'DESC']],
|
||||
order: [["created_at", "DESC"]],
|
||||
limit,
|
||||
offset
|
||||
offset,
|
||||
});
|
||||
|
||||
res.json(pageResult(rows.map(order => ({
|
||||
res.json(
|
||||
pageResult(
|
||||
rows.map((order) => ({
|
||||
id: order.id,
|
||||
orderNo: order.order_no,
|
||||
userId: order.user_id,
|
||||
@ -353,11 +458,16 @@ class PointsAdminController {
|
||||
storeId: order.store_id,
|
||||
storeName: order.store?.name,
|
||||
createdAt: order.created_at,
|
||||
verifiedAt: order.verified_at
|
||||
})), count, page, pageSize));
|
||||
verifiedAt: order.verified_at,
|
||||
})),
|
||||
count,
|
||||
page,
|
||||
pageSize,
|
||||
),
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('获取订单列表失败:', err);
|
||||
res.status(500).json(error('获取失败'));
|
||||
console.error("获取订单列表失败:", err);
|
||||
res.status(500).json(error("获取失败"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -369,23 +479,23 @@ class PointsAdminController {
|
||||
|
||||
const order = await PointOrder.findByPk(id);
|
||||
if (!order) {
|
||||
return res.status(404).json(error('订单不存在', 404));
|
||||
return res.status(404).json(error("订单不存在", 404));
|
||||
}
|
||||
|
||||
if (order.status !== ORDER_STATUS.PENDING) {
|
||||
return res.status(400).json(error('订单状态无效', 400));
|
||||
return res.status(400).json(error("订单状态无效", 400));
|
||||
}
|
||||
|
||||
await order.update({
|
||||
status: ORDER_STATUS.COMPLETED,
|
||||
verified_at: new Date(),
|
||||
verified_by: admin.id
|
||||
verified_by: admin.id,
|
||||
});
|
||||
|
||||
res.json(success(null, '核销成功'));
|
||||
res.json(success(null, "核销成功"));
|
||||
} catch (err) {
|
||||
console.error('核销订单失败:', err);
|
||||
res.status(500).json(error('核销失败'));
|
||||
console.error("核销订单失败:", err);
|
||||
res.status(500).json(error("核销失败"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -398,39 +508,46 @@ class PointsAdminController {
|
||||
const order = await PointOrder.findOne({
|
||||
where: { exchange_code },
|
||||
include: [
|
||||
{ model: User, as: 'user' },
|
||||
{ model: PointProduct, as: 'product' }
|
||||
]
|
||||
{ model: User, as: "user" },
|
||||
{ model: PointProduct, as: "product" },
|
||||
],
|
||||
});
|
||||
|
||||
if (!order) {
|
||||
return res.status(404).json(error('订单不存在', 404));
|
||||
return res.status(404).json(error("订单不存在", 404));
|
||||
}
|
||||
|
||||
// 验证门店权限
|
||||
if (admin.role !== 'super_admin' && order.store_id !== admin.store_id) {
|
||||
return res.status(403).json(error('该订单不属于您的门店', 403));
|
||||
if (admin.role !== "super_admin" && order.store_id !== admin.store_id) {
|
||||
return res.status(403).json(error("该订单不属于您的门店", 403));
|
||||
}
|
||||
|
||||
if (order.status !== ORDER_STATUS.PENDING) {
|
||||
return res.status(400).json(error(`订单${order.status === 1 ? '已核销' : '已取消'}`, 400));
|
||||
return res
|
||||
.status(400)
|
||||
.json(error(`订单${order.status === 1 ? "已核销" : "已取消"}`, 400));
|
||||
}
|
||||
|
||||
await order.update({
|
||||
status: ORDER_STATUS.COMPLETED,
|
||||
verified_at: new Date(),
|
||||
verified_by: admin.id
|
||||
verified_by: admin.id,
|
||||
});
|
||||
|
||||
res.json(success({
|
||||
res.json(
|
||||
success(
|
||||
{
|
||||
orderId: order.id,
|
||||
orderNo: order.order_no,
|
||||
productName: order.product_name,
|
||||
userNickname: order.user?.nickname
|
||||
}, '核销成功'));
|
||||
userNickname: order.user?.nickname,
|
||||
},
|
||||
"核销成功",
|
||||
),
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('核销订单失败:', err);
|
||||
res.status(500).json(error('核销失败'));
|
||||
console.error("核销订单失败:", err);
|
||||
res.status(500).json(error("核销失败"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,11 +1,18 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
const axios = require('axios');
|
||||
const crypto = require('crypto');
|
||||
const QRCode = require('qrcode');
|
||||
const { User, LadderUser, Store, Match, MatchGame } = require('../models');
|
||||
const { generateMemberCode, success, error, calculateDistance, getFullUrl, normalizeAvatarUrl } = require('../utils/helper');
|
||||
const { LADDER_LEVEL_NAMES } = require('../config/constants');
|
||||
const { Op } = require('sequelize');
|
||||
const jwt = require("jsonwebtoken");
|
||||
const axios = require("axios");
|
||||
const crypto = require("crypto");
|
||||
const QRCode = require("qrcode");
|
||||
const { User, LadderUser, Store, Match, MatchGame } = require("../models");
|
||||
const {
|
||||
generateMemberCode,
|
||||
success,
|
||||
error,
|
||||
calculateDistance,
|
||||
getFullUrl,
|
||||
normalizeAvatarUrl,
|
||||
} = require("../utils/helper");
|
||||
const { LADDER_LEVEL_NAMES } = require("../config/constants");
|
||||
const { Op } = require("sequelize");
|
||||
|
||||
class UserController {
|
||||
// 微信登录(获取 session_key,用于后续手机号解密)
|
||||
@ -14,21 +21,26 @@ class UserController {
|
||||
const { code } = req.body;
|
||||
|
||||
if (!code) {
|
||||
return res.status(400).json(error('缺少登录code', 400));
|
||||
return res.status(400).json(error("缺少登录code", 400));
|
||||
}
|
||||
|
||||
// 获取微信openid和session_key
|
||||
const wxRes = await axios.get('https://api.weixin.qq.com/sns/jscode2session', {
|
||||
const wxRes = await axios.get(
|
||||
"https://api.weixin.qq.com/sns/jscode2session",
|
||||
{
|
||||
params: {
|
||||
appid: process.env.WX_APPID,
|
||||
secret: process.env.WX_SECRET,
|
||||
js_code: code,
|
||||
grant_type: 'authorization_code'
|
||||
}
|
||||
});
|
||||
grant_type: "authorization_code",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (wxRes.data.errcode) {
|
||||
return res.status(400).json(error('微信登录失败: ' + wxRes.data.errmsg, 400));
|
||||
return res
|
||||
.status(400)
|
||||
.json(error("微信登录失败: " + wxRes.data.errmsg, 400));
|
||||
}
|
||||
|
||||
const { openid, unionid, session_key } = wxRes.data;
|
||||
@ -46,55 +58,77 @@ class UserController {
|
||||
// 这里为了简化流程,使用加密后的session_key
|
||||
const encryptedSessionKey = this.encryptSessionKey(session_key, openid);
|
||||
|
||||
res.json(success({
|
||||
res.json(
|
||||
success(
|
||||
{
|
||||
openid,
|
||||
unionid,
|
||||
sessionKey: encryptedSessionKey,
|
||||
isNewUser,
|
||||
hasPhone: user?.phone ? true : false,
|
||||
userInfo: user ? {
|
||||
userInfo: user
|
||||
? {
|
||||
id: user.id,
|
||||
nickname: user.nickname,
|
||||
avatar: user.avatar,
|
||||
phone: user.phone,
|
||||
gender: user.gender,
|
||||
memberCode: user.member_code,
|
||||
totalPoints: user.total_points
|
||||
} : null
|
||||
}, isNewUser ? '请授权手机号完成注册' : '登录成功'));
|
||||
totalPoints: user.total_points,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
isNewUser ? "请授权手机号完成注册" : "登录成功",
|
||||
),
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('登录失败:', err);
|
||||
res.status(500).json(error('登录失败'));
|
||||
}
|
||||
console.error("登录失败:", err);
|
||||
res.status(500).json(error("登录失败"));
|
||||
}
|
||||
};
|
||||
|
||||
// 加密session_key
|
||||
encryptSessionKey(sessionKey, openid) {
|
||||
const key = crypto.createHash('md5').update(process.env.JWT_SECRET + openid).digest();
|
||||
const key = crypto
|
||||
.createHash("md5")
|
||||
.update(process.env.JWT_SECRET + openid)
|
||||
.digest();
|
||||
const iv = Buffer.alloc(16, 0);
|
||||
const cipher = crypto.createCipheriv('aes-128-cbc', key, iv);
|
||||
let encrypted = cipher.update(sessionKey, 'utf8', 'base64');
|
||||
encrypted += cipher.final('base64');
|
||||
const cipher = crypto.createCipheriv("aes-128-cbc", key, iv);
|
||||
let encrypted = cipher.update(sessionKey, "utf8", "base64");
|
||||
encrypted += cipher.final("base64");
|
||||
return encrypted;
|
||||
}
|
||||
|
||||
// 解密session_key
|
||||
decryptSessionKey(encryptedSessionKey, openid) {
|
||||
const key = crypto.createHash('md5').update(process.env.JWT_SECRET + openid).digest();
|
||||
const key = crypto
|
||||
.createHash("md5")
|
||||
.update(process.env.JWT_SECRET + openid)
|
||||
.digest();
|
||||
const iv = Buffer.alloc(16, 0);
|
||||
const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv);
|
||||
let decrypted = decipher.update(encryptedSessionKey, 'base64', 'utf8');
|
||||
decrypted += decipher.final('utf8');
|
||||
const decipher = crypto.createDecipheriv("aes-128-cbc", key, iv);
|
||||
let decrypted = decipher.update(encryptedSessionKey, "base64", "utf8");
|
||||
decrypted += decipher.final("utf8");
|
||||
return decrypted;
|
||||
}
|
||||
|
||||
// 手机号授权登录(解密手机号并完成注册/登录)
|
||||
phoneLogin = async (req, res) => {
|
||||
try {
|
||||
const { openid, unionid, sessionKey, encryptedData, iv, nickname, avatar, gender } = req.body;
|
||||
const {
|
||||
openid,
|
||||
unionid,
|
||||
sessionKey,
|
||||
encryptedData,
|
||||
iv,
|
||||
nickname,
|
||||
avatar,
|
||||
gender,
|
||||
} = req.body;
|
||||
|
||||
if (!openid || !sessionKey || !encryptedData || !iv) {
|
||||
return res.status(400).json(error('参数不完整', 400));
|
||||
return res.status(400).json(error("参数不完整", 400));
|
||||
}
|
||||
|
||||
// 解密session_key
|
||||
@ -102,7 +136,7 @@ class UserController {
|
||||
try {
|
||||
realSessionKey = this.decryptSessionKey(sessionKey, openid);
|
||||
} catch (e) {
|
||||
return res.status(400).json(error('会话已过期,请重新登录', 400));
|
||||
return res.status(400).json(error("会话已过期,请重新登录", 400));
|
||||
}
|
||||
|
||||
// 解密手机号
|
||||
@ -110,12 +144,24 @@ class UserController {
|
||||
try {
|
||||
phone = this.decryptPhoneNumber(realSessionKey, encryptedData, iv);
|
||||
} catch (e) {
|
||||
console.error('手机号解密失败:', e);
|
||||
return res.status(400).json(error('手机号解密失败,请重新授权', 400));
|
||||
console.error("手机号解密失败:", e);
|
||||
return res.status(400).json(error("手机号解密失败,请重新授权", 400));
|
||||
}
|
||||
|
||||
if (!phone) {
|
||||
return res.status(400).json(error('获取手机号失败', 400));
|
||||
return res.status(400).json(error("获取手机号失败", 400));
|
||||
}
|
||||
|
||||
const normalizedGender =
|
||||
gender === undefined || gender === null || gender === ""
|
||||
? undefined
|
||||
: Number(gender);
|
||||
if (
|
||||
normalizedGender !== undefined &&
|
||||
normalizedGender !== 1 &&
|
||||
normalizedGender !== 2
|
||||
) {
|
||||
return res.status(400).json(error("性别参数无效", 400));
|
||||
}
|
||||
|
||||
// 查找或创建用户
|
||||
@ -126,33 +172,47 @@ class UserController {
|
||||
// 只有当 openid 是自动生成的格式时才允许合并
|
||||
const existingUser = await User.findOne({ where: { phone } });
|
||||
|
||||
if (existingUser && existingUser.openid.startsWith('AUTO_GEN_')) {
|
||||
if (existingUser && existingUser.openid.startsWith("AUTO_GEN_")) {
|
||||
// 合并账号:更新 openid 为真实的微信 openid
|
||||
console.log(`合并账号: 将自动生成用户 ${existingUser.id} (${existingUser.openid}) 更新为微信用户 ${openid}`);
|
||||
console.log(
|
||||
`合并账号: 将自动生成用户 ${existingUser.id} (${existingUser.openid}) 更新为微信用户 ${openid}`,
|
||||
);
|
||||
|
||||
const updateData = {
|
||||
openid,
|
||||
unionid
|
||||
unionid,
|
||||
};
|
||||
if (nickname) updateData.nickname = nickname;
|
||||
if (avatar) updateData.avatar = normalizeAvatarUrl(avatar, req);
|
||||
if (gender !== undefined) updateData.gender = gender;
|
||||
if (
|
||||
(existingUser.gender === 0 || existingUser.gender === null) &&
|
||||
normalizedGender === undefined
|
||||
) {
|
||||
return res.status(400).json(error("性别必填", 400));
|
||||
}
|
||||
if (normalizedGender !== undefined)
|
||||
updateData.gender = normalizedGender;
|
||||
|
||||
await existingUser.update(updateData);
|
||||
user = existingUser;
|
||||
} else {
|
||||
// 新用户注册
|
||||
if (normalizedGender === undefined) {
|
||||
return res.status(400).json(error("性别必填", 400));
|
||||
}
|
||||
// 规范化头像URL:如果是 https 则保持 https,否则是 http
|
||||
const normalizedAvatar = avatar ? normalizeAvatarUrl(avatar, req) : '';
|
||||
const normalizedAvatar = avatar
|
||||
? normalizeAvatarUrl(avatar, req)
|
||||
: "";
|
||||
user = await User.create({
|
||||
openid,
|
||||
unionid,
|
||||
phone,
|
||||
member_code: generateMemberCode(),
|
||||
nickname: nickname || '新用户',
|
||||
nickname: nickname || "新用户",
|
||||
avatar: normalizedAvatar,
|
||||
gender: gender || 0,
|
||||
status: 1
|
||||
gender: normalizedGender,
|
||||
status: 1,
|
||||
});
|
||||
|
||||
// 关联已存在的天梯用户(通过手机号)
|
||||
@ -164,7 +224,8 @@ class UserController {
|
||||
if (nickname) updateData.nickname = nickname;
|
||||
// 规范化头像URL:如果是 https 则保持 https,否则是 http
|
||||
if (avatar) updateData.avatar = normalizeAvatarUrl(avatar, req);
|
||||
if (gender !== undefined) updateData.gender = gender;
|
||||
if (normalizedGender !== undefined)
|
||||
updateData.gender = normalizedGender;
|
||||
|
||||
await user.update(updateData);
|
||||
|
||||
@ -173,19 +234,19 @@ class UserController {
|
||||
}
|
||||
|
||||
// 生成token
|
||||
const token = jwt.sign(
|
||||
{ userId: user.id },
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: process.env.JWT_EXPIRES_IN || '7d' }
|
||||
);
|
||||
const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET, {
|
||||
expiresIn: process.env.JWT_EXPIRES_IN || "7d",
|
||||
});
|
||||
|
||||
// 获取关联的天梯用户信息
|
||||
const ladderUsers = await LadderUser.findAll({
|
||||
where: { user_id: user.id, status: 1 },
|
||||
include: [{ model: Store, as: 'store', attributes: ['id', 'name'] }]
|
||||
include: [{ model: Store, as: "store", attributes: ["id", "name"] }],
|
||||
});
|
||||
|
||||
res.json(success({
|
||||
res.json(
|
||||
success(
|
||||
{
|
||||
token,
|
||||
userInfo: {
|
||||
id: user.id,
|
||||
@ -195,34 +256,41 @@ class UserController {
|
||||
gender: user.gender,
|
||||
memberCode: user.member_code,
|
||||
totalPoints: user.total_points,
|
||||
ladderUsers: ladderUsers.map(lu => ({
|
||||
ladderUsers: ladderUsers.map((lu) => ({
|
||||
id: lu.id,
|
||||
storeId: lu.store_id,
|
||||
storeName: lu.store?.name,
|
||||
realName: lu.real_name,
|
||||
level: lu.level,
|
||||
levelName: LADDER_LEVEL_NAMES[lu.level] || `Lv${lu.level}`,
|
||||
powerScore: lu.power_score
|
||||
}))
|
||||
}
|
||||
}, '登录成功'));
|
||||
powerScore: lu.power_score,
|
||||
})),
|
||||
},
|
||||
},
|
||||
"登录成功",
|
||||
),
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('手机号登录失败:', err);
|
||||
res.status(500).json(error('登录失败'));
|
||||
}
|
||||
console.error("手机号登录失败:", err);
|
||||
res.status(500).json(error("登录失败"));
|
||||
}
|
||||
};
|
||||
|
||||
// 解密微信手机号
|
||||
decryptPhoneNumber(sessionKey, encryptedData, iv) {
|
||||
const sessionKeyBuffer = Buffer.from(sessionKey, 'base64');
|
||||
const encryptedDataBuffer = Buffer.from(encryptedData, 'base64');
|
||||
const ivBuffer = Buffer.from(iv, 'base64');
|
||||
const sessionKeyBuffer = Buffer.from(sessionKey, "base64");
|
||||
const encryptedDataBuffer = Buffer.from(encryptedData, "base64");
|
||||
const ivBuffer = Buffer.from(iv, "base64");
|
||||
|
||||
const decipher = crypto.createDecipheriv('aes-128-cbc', sessionKeyBuffer, ivBuffer);
|
||||
const decipher = crypto.createDecipheriv(
|
||||
"aes-128-cbc",
|
||||
sessionKeyBuffer,
|
||||
ivBuffer,
|
||||
);
|
||||
decipher.setAutoPadding(true);
|
||||
|
||||
let decoded = decipher.update(encryptedDataBuffer, 'binary', 'utf8');
|
||||
decoded += decipher.final('utf8');
|
||||
let decoded = decipher.update(encryptedDataBuffer, "binary", "utf8");
|
||||
decoded += decipher.final("utf8");
|
||||
|
||||
const result = JSON.parse(decoded);
|
||||
return result.phoneNumber || result.purePhoneNumber;
|
||||
@ -235,17 +303,19 @@ class UserController {
|
||||
where: {
|
||||
phone,
|
||||
user_id: null,
|
||||
status: 1
|
||||
}
|
||||
status: 1,
|
||||
},
|
||||
});
|
||||
|
||||
// 批量更新关联
|
||||
if (unlinkedLadderUsers.length > 0) {
|
||||
await LadderUser.update(
|
||||
{ user_id: userId },
|
||||
{ where: { phone, user_id: null } }
|
||||
{ where: { phone, user_id: null } },
|
||||
);
|
||||
console.log(
|
||||
`已关联 ${unlinkedLadderUsers.length} 个天梯用户到用户 ${userId}`,
|
||||
);
|
||||
console.log(`已关联 ${unlinkedLadderUsers.length} 个天梯用户到用户 ${userId}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -259,20 +329,31 @@ class UserController {
|
||||
if (nickname) updateData.nickname = nickname;
|
||||
// 规范化头像URL:如果是 https 则保持 https,否则是 http
|
||||
if (avatar) updateData.avatar = normalizeAvatarUrl(avatar, req);
|
||||
if (gender !== undefined) updateData.gender = gender;
|
||||
if (gender !== undefined) {
|
||||
const normalizedGender = Number(gender);
|
||||
if (normalizedGender !== 1 && normalizedGender !== 2) {
|
||||
return res.status(400).json(error("性别参数无效", 400));
|
||||
}
|
||||
updateData.gender = normalizedGender;
|
||||
}
|
||||
|
||||
await user.update(updateData);
|
||||
|
||||
res.json(success({
|
||||
res.json(
|
||||
success(
|
||||
{
|
||||
nickname: user.nickname,
|
||||
avatar: getFullUrl(user.avatar, req),
|
||||
gender: user.gender
|
||||
}, '更新成功'));
|
||||
gender: user.gender,
|
||||
},
|
||||
"更新成功",
|
||||
),
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('更新资料失败:', err);
|
||||
res.status(500).json(error('更新失败'));
|
||||
}
|
||||
console.error("更新资料失败:", err);
|
||||
res.status(500).json(error("更新失败"));
|
||||
}
|
||||
};
|
||||
|
||||
// 获取用户信息
|
||||
getInfo = async (req, res) => {
|
||||
@ -282,10 +363,11 @@ class UserController {
|
||||
// 获取天梯信息
|
||||
const ladderUsers = await LadderUser.findAll({
|
||||
where: { user_id: user.id, status: 1 },
|
||||
include: [{ model: Store, as: 'store', attributes: ['id', 'name'] }]
|
||||
include: [{ model: Store, as: "store", attributes: ["id", "name"] }],
|
||||
});
|
||||
|
||||
res.json(success({
|
||||
res.json(
|
||||
success({
|
||||
id: user.id,
|
||||
nickname: user.nickname,
|
||||
avatar: getFullUrl(user.avatar, req),
|
||||
@ -293,7 +375,7 @@ class UserController {
|
||||
gender: user.gender,
|
||||
memberCode: user.member_code,
|
||||
totalPoints: user.total_points,
|
||||
ladderUsers: ladderUsers.map(lu => ({
|
||||
ladderUsers: ladderUsers.map((lu) => ({
|
||||
id: lu.id,
|
||||
storeId: lu.store_id,
|
||||
storeName: lu.store?.name,
|
||||
@ -302,14 +384,15 @@ class UserController {
|
||||
levelName: LADDER_LEVEL_NAMES[lu.level] || `Lv${lu.level}`,
|
||||
powerScore: lu.power_score,
|
||||
matchCount: lu.match_count,
|
||||
winCount: lu.win_count
|
||||
}))
|
||||
}));
|
||||
winCount: lu.win_count,
|
||||
})),
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('获取用户信息失败:', err);
|
||||
res.status(500).json(error('获取用户信息失败'));
|
||||
}
|
||||
console.error("获取用户信息失败:", err);
|
||||
res.status(500).json(error("获取用户信息失败"));
|
||||
}
|
||||
};
|
||||
|
||||
// 更新用户信息
|
||||
updateInfo = async (req, res) => {
|
||||
@ -321,27 +404,29 @@ class UserController {
|
||||
nickname: nickname || user.nickname,
|
||||
avatar: avatar || user.avatar,
|
||||
phone: phone || user.phone,
|
||||
gender: gender !== undefined ? gender : user.gender
|
||||
gender: gender !== undefined ? gender : user.gender,
|
||||
});
|
||||
|
||||
res.json(success(null, '更新成功'));
|
||||
res.json(success(null, "更新成功"));
|
||||
} catch (err) {
|
||||
console.error('更新用户信息失败:', err);
|
||||
res.status(500).json(error('更新失败'));
|
||||
}
|
||||
console.error("更新用户信息失败:", err);
|
||||
res.status(500).json(error("更新失败"));
|
||||
}
|
||||
};
|
||||
|
||||
// 获取会员码
|
||||
getMemberCode = async (req, res) => {
|
||||
try {
|
||||
res.json(success({
|
||||
memberCode: req.user.member_code
|
||||
}));
|
||||
res.json(
|
||||
success({
|
||||
memberCode: req.user.member_code,
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('获取会员码失败:', err);
|
||||
res.status(500).json(error('获取失败'));
|
||||
}
|
||||
console.error("获取会员码失败:", err);
|
||||
res.status(500).json(error("获取失败"));
|
||||
}
|
||||
};
|
||||
|
||||
// 通过会员码查询用户
|
||||
getByMemberCode = async (req, res) => {
|
||||
@ -350,38 +435,42 @@ class UserController {
|
||||
const { store_id } = req.query;
|
||||
|
||||
const user = await User.findOne({
|
||||
where: { member_code: code, status: 1 }
|
||||
where: { member_code: code, status: 1 },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json(error('用户不存在', 404));
|
||||
return res.status(404).json(error("用户不存在", 404));
|
||||
}
|
||||
|
||||
// 获取该门店的天梯信息
|
||||
let ladderUser = null;
|
||||
if (store_id) {
|
||||
ladderUser = await LadderUser.findOne({
|
||||
where: { user_id: user.id, store_id, status: 1 }
|
||||
where: { user_id: user.id, store_id, status: 1 },
|
||||
});
|
||||
}
|
||||
|
||||
res.json(success({
|
||||
res.json(
|
||||
success({
|
||||
id: user.id,
|
||||
nickname: user.nickname,
|
||||
avatar: user.avatar,
|
||||
gender: user.gender,
|
||||
ladderUser: ladderUser ? {
|
||||
ladderUser: ladderUser
|
||||
? {
|
||||
id: ladderUser.id,
|
||||
realName: ladderUser.real_name,
|
||||
level: ladderUser.level,
|
||||
powerScore: ladderUser.power_score
|
||||
} : null
|
||||
}));
|
||||
powerScore: ladderUser.power_score,
|
||||
}
|
||||
: null,
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('查询用户失败:', err);
|
||||
res.status(500).json(error('查询失败'));
|
||||
}
|
||||
console.error("查询用户失败:", err);
|
||||
res.status(500).json(error("查询失败"));
|
||||
}
|
||||
};
|
||||
|
||||
// 获取用户天梯信息
|
||||
getLadderInfo = async (req, res) => {
|
||||
@ -396,10 +485,12 @@ class UserController {
|
||||
|
||||
const ladderUsers = await LadderUser.findAll({
|
||||
where,
|
||||
include: [{ model: Store, as: 'store', attributes: ['id', 'name'] }]
|
||||
include: [{ model: Store, as: "store", attributes: ["id", "name"] }],
|
||||
});
|
||||
|
||||
res.json(success(ladderUsers.map(lu => ({
|
||||
res.json(
|
||||
success(
|
||||
ladderUsers.map((lu) => ({
|
||||
id: lu.id,
|
||||
storeId: lu.store_id,
|
||||
storeName: lu.store?.name,
|
||||
@ -411,13 +502,15 @@ class UserController {
|
||||
matchCount: lu.match_count,
|
||||
winCount: lu.win_count,
|
||||
monthlyMatchCount: lu.monthly_match_count,
|
||||
lastMatchTime: lu.last_match_time
|
||||
}))));
|
||||
lastMatchTime: lu.last_match_time,
|
||||
})),
|
||||
),
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('获取天梯信息失败:', err);
|
||||
res.status(500).json(error('获取失败'));
|
||||
}
|
||||
console.error("获取天梯信息失败:", err);
|
||||
res.status(500).json(error("获取失败"));
|
||||
}
|
||||
};
|
||||
|
||||
// 获取当前门店
|
||||
getCurrentStore = async (req, res) => {
|
||||
@ -428,26 +521,28 @@ class UserController {
|
||||
// 获取用户参与天梯的门店
|
||||
const ladderUsers = await LadderUser.findAll({
|
||||
where: { user_id: user.id, status: 1 },
|
||||
include: [{ model: Store, as: 'store' }],
|
||||
order: [['last_match_time', 'DESC']]
|
||||
include: [{ model: Store, as: "store" }],
|
||||
order: [["last_match_time", "DESC"]],
|
||||
});
|
||||
|
||||
if (ladderUsers.length > 0) {
|
||||
// 有参与天梯的门店,返回最近参与比赛的门店
|
||||
const lu = ladderUsers[0];
|
||||
return res.json(success({
|
||||
return res.json(
|
||||
success({
|
||||
storeId: lu.store_id,
|
||||
storeName: lu.store?.name,
|
||||
storeAddress: lu.store?.address,
|
||||
ladderUserId: lu.id,
|
||||
source: 'last_match'
|
||||
}));
|
||||
source: "last_match",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// 没有天梯门店,返回最近的门店
|
||||
if (latitude && longitude) {
|
||||
const stores = await Store.findAll({
|
||||
where: { status: 1 }
|
||||
where: { status: 1 },
|
||||
});
|
||||
|
||||
if (stores.length > 0) {
|
||||
@ -460,7 +555,7 @@ class UserController {
|
||||
parseFloat(latitude),
|
||||
parseFloat(longitude),
|
||||
parseFloat(store.latitude),
|
||||
parseFloat(store.longitude)
|
||||
parseFloat(store.longitude),
|
||||
);
|
||||
if (dist < minDistance) {
|
||||
minDistance = dist;
|
||||
@ -469,30 +564,38 @@ class UserController {
|
||||
}
|
||||
}
|
||||
|
||||
return res.json(success({
|
||||
return res.json(
|
||||
success({
|
||||
storeId: nearestStore.id,
|
||||
storeName: nearestStore.name,
|
||||
storeAddress: nearestStore.address,
|
||||
ladderUserId: null,
|
||||
source: 'nearest'
|
||||
}));
|
||||
source: "nearest",
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 返回第一个门店
|
||||
const firstStore = await Store.findOne({ where: { status: 1 } });
|
||||
res.json(success(firstStore ? {
|
||||
res.json(
|
||||
success(
|
||||
firstStore
|
||||
? {
|
||||
storeId: firstStore.id,
|
||||
storeName: firstStore.name,
|
||||
storeAddress: firstStore.address,
|
||||
ladderUserId: null,
|
||||
source: 'default'
|
||||
} : null));
|
||||
source: "default",
|
||||
}
|
||||
: null,
|
||||
),
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('获取当前门店失败:', err);
|
||||
res.status(500).json(error('获取失败'));
|
||||
}
|
||||
console.error("获取当前门店失败:", err);
|
||||
res.status(500).json(error("获取失败"));
|
||||
}
|
||||
};
|
||||
|
||||
// 生成会员二维码图片
|
||||
getQrcode = async (req, res) => {
|
||||
@ -500,33 +603,35 @@ class UserController {
|
||||
const user = req.user;
|
||||
|
||||
if (!user.member_code) {
|
||||
return res.status(400).json(error('会员码不存在', 400));
|
||||
return res.status(400).json(error("会员码不存在", 400));
|
||||
}
|
||||
|
||||
// 生成二维码配置
|
||||
const qrOptions = {
|
||||
errorCorrectionLevel: 'M',
|
||||
type: 'image/png',
|
||||
errorCorrectionLevel: "M",
|
||||
type: "image/png",
|
||||
margin: 2,
|
||||
width: 300,
|
||||
color: {
|
||||
dark: '#1A1A1A',
|
||||
light: '#FFFFFF'
|
||||
}
|
||||
dark: "#1A1A1A",
|
||||
light: "#FFFFFF",
|
||||
},
|
||||
};
|
||||
|
||||
// 生成二维码为 base64
|
||||
const qrcodeDataUrl = await QRCode.toDataURL(user.member_code, qrOptions);
|
||||
|
||||
res.json(success({
|
||||
res.json(
|
||||
success({
|
||||
memberCode: user.member_code,
|
||||
qrcode: qrcodeDataUrl
|
||||
}));
|
||||
qrcode: qrcodeDataUrl,
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('生成二维码失败:', err);
|
||||
res.status(500).json(error('生成二维码失败'));
|
||||
}
|
||||
console.error("生成二维码失败:", err);
|
||||
res.status(500).json(error("生成二维码失败"));
|
||||
}
|
||||
};
|
||||
|
||||
// 直接返回二维码图片
|
||||
getQrcodeImage = async (req, res) => {
|
||||
@ -534,32 +639,32 @@ class UserController {
|
||||
const user = req.user;
|
||||
|
||||
if (!user.member_code) {
|
||||
return res.status(400).send('会员码不存在');
|
||||
return res.status(400).send("会员码不存在");
|
||||
}
|
||||
|
||||
// 生成二维码配置
|
||||
const qrOptions = {
|
||||
errorCorrectionLevel: 'M',
|
||||
type: 'png',
|
||||
errorCorrectionLevel: "M",
|
||||
type: "png",
|
||||
margin: 2,
|
||||
width: 300,
|
||||
color: {
|
||||
dark: '#1A1A1A',
|
||||
light: '#FFFFFF'
|
||||
}
|
||||
dark: "#1A1A1A",
|
||||
light: "#FFFFFF",
|
||||
},
|
||||
};
|
||||
|
||||
// 设置响应头
|
||||
res.setHeader('Content-Type', 'image/png');
|
||||
res.setHeader('Cache-Control', 'public, max-age=86400');
|
||||
res.setHeader("Content-Type", "image/png");
|
||||
res.setHeader("Cache-Control", "public, max-age=86400");
|
||||
|
||||
// 直接输出二维码图片
|
||||
await QRCode.toFileStream(res, user.member_code, qrOptions);
|
||||
} catch (err) {
|
||||
console.error('生成二维码图片失败:', err);
|
||||
res.status(500).send('生成二维码失败');
|
||||
}
|
||||
console.error("生成二维码图片失败:", err);
|
||||
res.status(500).send("生成二维码失败");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = new UserController();
|
||||
|
||||
@ -1,18 +1,21 @@
|
||||
const express = require('express');
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const ladderController = require('../controllers/ladderController');
|
||||
const { authUser } = require('../middlewares/auth');
|
||||
const ladderController = require("../controllers/ladderController");
|
||||
const { authUser } = require("../middlewares/auth");
|
||||
|
||||
// 获取天梯排名列表
|
||||
router.get('/ranking', ladderController.getRanking);
|
||||
router.get("/ranking", ladderController.getRanking);
|
||||
|
||||
// 获取我的排名(用于小程序定位)
|
||||
router.get("/my-rank", authUser, ladderController.getMyRank);
|
||||
|
||||
// 获取天梯用户详情
|
||||
router.get('/user/:id', ladderController.getUserDetail);
|
||||
router.get("/user/:id", ladderController.getUserDetail);
|
||||
|
||||
// 选手详情(兼容小程序端:/api/ladder/player?id=xxx)
|
||||
router.get('/player', ladderController.getPlayerDetail);
|
||||
router.get("/player", ladderController.getPlayerDetail);
|
||||
|
||||
// 获取等级说明
|
||||
router.get('/levels', ladderController.getLevelInfo);
|
||||
router.get("/levels", ladderController.getLevelInfo);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@ -1,60 +1,87 @@
|
||||
const express = require('express');
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const matchController = require('../controllers/matchController');
|
||||
const { authUser, requireLadderUser } = require('../middlewares/auth');
|
||||
const matchController = require("../controllers/matchController");
|
||||
const { authUser, requireLadderUser } = require("../middlewares/auth");
|
||||
|
||||
// === 挑战赛 ===
|
||||
// 检查是否可以发起挑战
|
||||
router.get('/challenge/check/:targetMemberCode', authUser, matchController.checkChallenge);
|
||||
router.get(
|
||||
"/challenge/check/:targetMemberCode",
|
||||
authUser,
|
||||
matchController.checkChallenge,
|
||||
);
|
||||
|
||||
// 发起挑战
|
||||
router.post('/challenge/create', authUser, matchController.createChallenge);
|
||||
router.post("/challenge/create", authUser, matchController.createChallenge);
|
||||
|
||||
// 响应挑战
|
||||
router.post('/challenge/respond', authUser, matchController.respondChallenge);
|
||||
router.post("/challenge/respond", authUser, matchController.respondChallenge);
|
||||
|
||||
// 提交比分
|
||||
router.post('/challenge/submit-score', authUser, matchController.submitScore);
|
||||
router.post("/challenge/submit-score", authUser, matchController.submitScore);
|
||||
|
||||
// 确认比分
|
||||
router.post('/challenge/confirm-score', authUser, matchController.confirmScore);
|
||||
router.post("/challenge/confirm-score", authUser, matchController.confirmScore);
|
||||
|
||||
// === 排位赛 ===
|
||||
// 扫码加入排位赛
|
||||
router.post('/ranking/join', authUser, matchController.joinRankingMatch);
|
||||
router.post("/ranking/join", authUser, matchController.joinRankingMatch);
|
||||
|
||||
// 获取排位赛详情
|
||||
router.get('/ranking/:matchCode', authUser, matchController.getRankingDetail);
|
||||
router.get("/ranking/:matchCode", authUser, matchController.getRankingDetail);
|
||||
|
||||
// 获取我的当前对局
|
||||
router.get('/ranking/:matchCode/my-game', authUser, matchController.getMyCurrentGame);
|
||||
router.get(
|
||||
"/ranking/:matchCode/my-game",
|
||||
authUser,
|
||||
matchController.getMyCurrentGame,
|
||||
);
|
||||
|
||||
// 提交排位赛比分
|
||||
router.post('/ranking/submit-score', authUser, matchController.submitRankingScore);
|
||||
router.post(
|
||||
"/ranking/submit-score",
|
||||
authUser,
|
||||
matchController.submitRankingScore,
|
||||
);
|
||||
|
||||
// 确认排位赛比分
|
||||
router.post('/ranking/confirm-score', authUser, matchController.confirmRankingScore);
|
||||
router.post(
|
||||
"/ranking/confirm-score",
|
||||
authUser,
|
||||
matchController.confirmRankingScore,
|
||||
);
|
||||
|
||||
// === 裁判操作 ===
|
||||
// 裁判修改比分
|
||||
router.post('/referee/update-score', authUser, matchController.refereeUpdateScore);
|
||||
router.post(
|
||||
"/referee/update-score",
|
||||
authUser,
|
||||
matchController.refereeUpdateScore,
|
||||
);
|
||||
// 裁判开始淘汰赛
|
||||
router.post('/referee/start-elimination', authUser, matchController.refereeStartElimination);
|
||||
router.post(
|
||||
"/referee/start-elimination",
|
||||
authUser,
|
||||
matchController.refereeStartElimination,
|
||||
);
|
||||
|
||||
// === 通用 ===
|
||||
// 获取正在进行中的比赛
|
||||
router.get('/ongoing', authUser, matchController.getOngoingMatches);
|
||||
router.get("/ongoing", authUser, matchController.getOngoingMatches);
|
||||
|
||||
// 大屏:进行中/近7天比赛列表(公开)
|
||||
router.get("/display-list", matchController.getDisplayList);
|
||||
|
||||
// 获取选手比赛记录(用于选手详情页)
|
||||
router.get('/history', authUser, matchController.getPlayerHistory);
|
||||
router.get("/history", authUser, matchController.getPlayerHistory);
|
||||
|
||||
// 获取我的比赛记录
|
||||
router.get('/my-matches', authUser, matchController.getMyMatches);
|
||||
router.get("/my-matches", authUser, matchController.getMyMatches);
|
||||
|
||||
// 获取待确认的比赛
|
||||
router.get('/pending-confirm', authUser, matchController.getPendingConfirm);
|
||||
router.get("/pending-confirm", authUser, matchController.getPendingConfirm);
|
||||
|
||||
// 获取比赛详情
|
||||
router.get('/:id', authUser, matchController.getMatchDetail);
|
||||
router.get("/:id", authUser, matchController.getMatchDetail);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
23
server/src/scripts/setChallengeMatchWeightTo1.js
Normal file
23
server/src/scripts/setChallengeMatchWeightTo1.js
Normal file
@ -0,0 +1,23 @@
|
||||
const sequelize = require("../config/database");
|
||||
const { Match } = require("../models");
|
||||
const { MATCH_TYPES } = require("../config/constants");
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
const [affected] = await Match.update(
|
||||
{ weight: 1.0 },
|
||||
{ where: { type: MATCH_TYPES.CHALLENGE } },
|
||||
);
|
||||
console.log(`已更新挑战赛权重为 1.0,影响行数: ${affected}`);
|
||||
await sequelize.close();
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
console.error("更新挑战赛权重失败:", err);
|
||||
try {
|
||||
await sequelize.close();
|
||||
} catch (e) {}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
Loading…
Reference in New Issue
Block a user