Update dependencies, enhance match view with QR code functionality, and improve user login process
112
admin/node_modules/.vite/deps/_metadata.json
generated
vendored
@ -2,301 +2,307 @@
|
|||||||
"hash": "72015d08",
|
"hash": "72015d08",
|
||||||
"configHash": "0bd4dba1",
|
"configHash": "0bd4dba1",
|
||||||
"lockfileHash": "39601b45",
|
"lockfileHash": "39601b45",
|
||||||
"browserHash": "a4d88a8a",
|
"browserHash": "b4faa0d4",
|
||||||
"optimized": {
|
"optimized": {
|
||||||
"@element-plus/icons-vue": {
|
"@element-plus/icons-vue": {
|
||||||
"src": "../../@element-plus/icons-vue/dist/index.js",
|
"src": "../../@element-plus/icons-vue/dist/index.js",
|
||||||
"file": "@element-plus_icons-vue.js",
|
"file": "@element-plus_icons-vue.js",
|
||||||
"fileHash": "7f008ba6",
|
"fileHash": "a0465eac",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"axios": {
|
"axios": {
|
||||||
"src": "../../axios/index.js",
|
"src": "../../axios/index.js",
|
||||||
"file": "axios.js",
|
"file": "axios.js",
|
||||||
"fileHash": "3f18dca8",
|
"fileHash": "5dd68ace",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"dayjs": {
|
"dayjs": {
|
||||||
"src": "../../dayjs/dayjs.min.js",
|
"src": "../../dayjs/dayjs.min.js",
|
||||||
"file": "dayjs.js",
|
"file": "dayjs.js",
|
||||||
"fileHash": "58ba4ccf",
|
"fileHash": "43487a3d",
|
||||||
"needsInterop": true
|
"needsInterop": true
|
||||||
},
|
},
|
||||||
"element-plus": {
|
"element-plus": {
|
||||||
"src": "../../element-plus/es/index.mjs",
|
"src": "../../element-plus/es/index.mjs",
|
||||||
"file": "element-plus.js",
|
"file": "element-plus.js",
|
||||||
"fileHash": "3f46101b",
|
"fileHash": "641ecf31",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus/dist/locale/zh-cn.mjs": {
|
"element-plus/dist/locale/zh-cn.mjs": {
|
||||||
"src": "../../element-plus/dist/locale/zh-cn.mjs",
|
"src": "../../element-plus/dist/locale/zh-cn.mjs",
|
||||||
"file": "element-plus_dist_locale_zh-cn__mjs.js",
|
"file": "element-plus_dist_locale_zh-cn__mjs.js",
|
||||||
"fileHash": "7c78b45c",
|
"fileHash": "c69676ce",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"pinia": {
|
"pinia": {
|
||||||
"src": "../../pinia/dist/pinia.mjs",
|
"src": "../../pinia/dist/pinia.mjs",
|
||||||
"file": "pinia.js",
|
"file": "pinia.js",
|
||||||
"fileHash": "af5f9503",
|
"fileHash": "bfa2bd84",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"vue": {
|
"vue": {
|
||||||
"src": "../../vue/dist/vue.runtime.esm-bundler.js",
|
"src": "../../vue/dist/vue.runtime.esm-bundler.js",
|
||||||
"file": "vue.js",
|
"file": "vue.js",
|
||||||
"fileHash": "8671a44a",
|
"fileHash": "95941b62",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"vue-router": {
|
"vue-router": {
|
||||||
"src": "../../vue-router/dist/vue-router.mjs",
|
"src": "../../vue-router/dist/vue-router.mjs",
|
||||||
"file": "vue-router.js",
|
"file": "vue-router.js",
|
||||||
"fileHash": "e5335c2b",
|
"fileHash": "799d6557",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus/es": {
|
"element-plus/es": {
|
||||||
"src": "../../element-plus/es/index.mjs",
|
"src": "../../element-plus/es/index.mjs",
|
||||||
"file": "element-plus_es.js",
|
"file": "element-plus_es.js",
|
||||||
"fileHash": "294409a4",
|
"fileHash": "bf7435a6",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus/es/components/base/style/css": {
|
"element-plus/es/components/base/style/css": {
|
||||||
"src": "../../element-plus/es/components/base/style/css.mjs",
|
"src": "../../element-plus/es/components/base/style/css.mjs",
|
||||||
"file": "element-plus_es_components_base_style_css.js",
|
"file": "element-plus_es_components_base_style_css.js",
|
||||||
"fileHash": "6f0ffffd",
|
"fileHash": "69992cb3",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus/es/components/form/style/css": {
|
"element-plus/es/components/form/style/css": {
|
||||||
"src": "../../element-plus/es/components/form/style/css.mjs",
|
"src": "../../element-plus/es/components/form/style/css.mjs",
|
||||||
"file": "element-plus_es_components_form_style_css.js",
|
"file": "element-plus_es_components_form_style_css.js",
|
||||||
"fileHash": "db280542",
|
"fileHash": "ebec77cf",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus/es/components/button/style/css": {
|
"element-plus/es/components/button/style/css": {
|
||||||
"src": "../../element-plus/es/components/button/style/css.mjs",
|
"src": "../../element-plus/es/components/button/style/css.mjs",
|
||||||
"file": "element-plus_es_components_button_style_css.js",
|
"file": "element-plus_es_components_button_style_css.js",
|
||||||
"fileHash": "647ba0c4",
|
"fileHash": "911a2184",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus/es/components/form-item/style/css": {
|
"element-plus/es/components/form-item/style/css": {
|
||||||
"src": "../../element-plus/es/components/form-item/style/css.mjs",
|
"src": "../../element-plus/es/components/form-item/style/css.mjs",
|
||||||
"file": "element-plus_es_components_form-item_style_css.js",
|
"file": "element-plus_es_components_form-item_style_css.js",
|
||||||
"fileHash": "df94624f",
|
"fileHash": "36203619",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus/es/components/input/style/css": {
|
"element-plus/es/components/input/style/css": {
|
||||||
"src": "../../element-plus/es/components/input/style/css.mjs",
|
"src": "../../element-plus/es/components/input/style/css.mjs",
|
||||||
"file": "element-plus_es_components_input_style_css.js",
|
"file": "element-plus_es_components_input_style_css.js",
|
||||||
"fileHash": "6ce4b79b",
|
"fileHash": "3cb1f96d",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus/es/components/dialog/style/css": {
|
"element-plus/es/components/dialog/style/css": {
|
||||||
"src": "../../element-plus/es/components/dialog/style/css.mjs",
|
"src": "../../element-plus/es/components/dialog/style/css.mjs",
|
||||||
"file": "element-plus_es_components_dialog_style_css.js",
|
"file": "element-plus_es_components_dialog_style_css.js",
|
||||||
"fileHash": "09e38fa4",
|
"fileHash": "76e79a0a",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus/es/components/container/style/css": {
|
"element-plus/es/components/container/style/css": {
|
||||||
"src": "../../element-plus/es/components/container/style/css.mjs",
|
"src": "../../element-plus/es/components/container/style/css.mjs",
|
||||||
"file": "element-plus_es_components_container_style_css.js",
|
"file": "element-plus_es_components_container_style_css.js",
|
||||||
"fileHash": "9f75a15b",
|
"fileHash": "3871ef81",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus/es/components/main/style/css": {
|
"element-plus/es/components/main/style/css": {
|
||||||
"src": "../../element-plus/es/components/main/style/css.mjs",
|
"src": "../../element-plus/es/components/main/style/css.mjs",
|
||||||
"file": "element-plus_es_components_main_style_css.js",
|
"file": "element-plus_es_components_main_style_css.js",
|
||||||
"fileHash": "b7d52e31",
|
"fileHash": "3d6a71ca",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus/es/components/header/style/css": {
|
"element-plus/es/components/header/style/css": {
|
||||||
"src": "../../element-plus/es/components/header/style/css.mjs",
|
"src": "../../element-plus/es/components/header/style/css.mjs",
|
||||||
"file": "element-plus_es_components_header_style_css.js",
|
"file": "element-plus_es_components_header_style_css.js",
|
||||||
"fileHash": "dc7cbd51",
|
"fileHash": "88603798",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus/es/components/dropdown/style/css": {
|
"element-plus/es/components/dropdown/style/css": {
|
||||||
"src": "../../element-plus/es/components/dropdown/style/css.mjs",
|
"src": "../../element-plus/es/components/dropdown/style/css.mjs",
|
||||||
"file": "element-plus_es_components_dropdown_style_css.js",
|
"file": "element-plus_es_components_dropdown_style_css.js",
|
||||||
"fileHash": "c25ab9b5",
|
"fileHash": "9e766544",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus/es/components/dropdown-menu/style/css": {
|
"element-plus/es/components/dropdown-menu/style/css": {
|
||||||
"src": "../../element-plus/es/components/dropdown-menu/style/css.mjs",
|
"src": "../../element-plus/es/components/dropdown-menu/style/css.mjs",
|
||||||
"file": "element-plus_es_components_dropdown-menu_style_css.js",
|
"file": "element-plus_es_components_dropdown-menu_style_css.js",
|
||||||
"fileHash": "36878229",
|
"fileHash": "b8031298",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus/es/components/dropdown-item/style/css": {
|
"element-plus/es/components/dropdown-item/style/css": {
|
||||||
"src": "../../element-plus/es/components/dropdown-item/style/css.mjs",
|
"src": "../../element-plus/es/components/dropdown-item/style/css.mjs",
|
||||||
"file": "element-plus_es_components_dropdown-item_style_css.js",
|
"file": "element-plus_es_components_dropdown-item_style_css.js",
|
||||||
"fileHash": "3f098ecd",
|
"fileHash": "a1a95087",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus/es/components/avatar/style/css": {
|
"element-plus/es/components/avatar/style/css": {
|
||||||
"src": "../../element-plus/es/components/avatar/style/css.mjs",
|
"src": "../../element-plus/es/components/avatar/style/css.mjs",
|
||||||
"file": "element-plus_es_components_avatar_style_css.js",
|
"file": "element-plus_es_components_avatar_style_css.js",
|
||||||
"fileHash": "a302ad09",
|
"fileHash": "9aca49c4",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus/es/components/breadcrumb/style/css": {
|
"element-plus/es/components/breadcrumb/style/css": {
|
||||||
"src": "../../element-plus/es/components/breadcrumb/style/css.mjs",
|
"src": "../../element-plus/es/components/breadcrumb/style/css.mjs",
|
||||||
"file": "element-plus_es_components_breadcrumb_style_css.js",
|
"file": "element-plus_es_components_breadcrumb_style_css.js",
|
||||||
"fileHash": "630e60e3",
|
"fileHash": "3bc001aa",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus/es/components/breadcrumb-item/style/css": {
|
"element-plus/es/components/breadcrumb-item/style/css": {
|
||||||
"src": "../../element-plus/es/components/breadcrumb-item/style/css.mjs",
|
"src": "../../element-plus/es/components/breadcrumb-item/style/css.mjs",
|
||||||
"file": "element-plus_es_components_breadcrumb-item_style_css.js",
|
"file": "element-plus_es_components_breadcrumb-item_style_css.js",
|
||||||
"fileHash": "3d33f916",
|
"fileHash": "be124fe0",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus/es/components/aside/style/css": {
|
"element-plus/es/components/aside/style/css": {
|
||||||
"src": "../../element-plus/es/components/aside/style/css.mjs",
|
"src": "../../element-plus/es/components/aside/style/css.mjs",
|
||||||
"file": "element-plus_es_components_aside_style_css.js",
|
"file": "element-plus_es_components_aside_style_css.js",
|
||||||
"fileHash": "52b6f7bc",
|
"fileHash": "c068ded5",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus/es/components/menu/style/css": {
|
"element-plus/es/components/menu/style/css": {
|
||||||
"src": "../../element-plus/es/components/menu/style/css.mjs",
|
"src": "../../element-plus/es/components/menu/style/css.mjs",
|
||||||
"file": "element-plus_es_components_menu_style_css.js",
|
"file": "element-plus_es_components_menu_style_css.js",
|
||||||
"fileHash": "77e7329d",
|
"fileHash": "403f5873",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus/es/components/menu-item/style/css": {
|
"element-plus/es/components/menu-item/style/css": {
|
||||||
"src": "../../element-plus/es/components/menu-item/style/css.mjs",
|
"src": "../../element-plus/es/components/menu-item/style/css.mjs",
|
||||||
"file": "element-plus_es_components_menu-item_style_css.js",
|
"file": "element-plus_es_components_menu-item_style_css.js",
|
||||||
"fileHash": "ae2a2097",
|
"fileHash": "27234326",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus/es/components/icon/style/css": {
|
"element-plus/es/components/icon/style/css": {
|
||||||
"src": "../../element-plus/es/components/icon/style/css.mjs",
|
"src": "../../element-plus/es/components/icon/style/css.mjs",
|
||||||
"file": "element-plus_es_components_icon_style_css.js",
|
"file": "element-plus_es_components_icon_style_css.js",
|
||||||
"fileHash": "c16e8e96",
|
"fileHash": "235345a9",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus/es/components/input-number/style/css": {
|
"element-plus/es/components/input-number/style/css": {
|
||||||
"src": "../../element-plus/es/components/input-number/style/css.mjs",
|
"src": "../../element-plus/es/components/input-number/style/css.mjs",
|
||||||
"file": "element-plus_es_components_input-number_style_css.js",
|
"file": "element-plus_es_components_input-number_style_css.js",
|
||||||
"fileHash": "0ef981fb",
|
"fileHash": "9b08cd36",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus/es/components/tag/style/css": {
|
"element-plus/es/components/tag/style/css": {
|
||||||
"src": "../../element-plus/es/components/tag/style/css.mjs",
|
"src": "../../element-plus/es/components/tag/style/css.mjs",
|
||||||
"file": "element-plus_es_components_tag_style_css.js",
|
"file": "element-plus_es_components_tag_style_css.js",
|
||||||
"fileHash": "7268bcfa",
|
"fileHash": "9f589faf",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus/es/components/row/style/css": {
|
"element-plus/es/components/row/style/css": {
|
||||||
"src": "../../element-plus/es/components/row/style/css.mjs",
|
"src": "../../element-plus/es/components/row/style/css.mjs",
|
||||||
"file": "element-plus_es_components_row_style_css.js",
|
"file": "element-plus_es_components_row_style_css.js",
|
||||||
"fileHash": "3ebd4979",
|
"fileHash": "638d7e8c",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus/es/components/col/style/css": {
|
"element-plus/es/components/col/style/css": {
|
||||||
"src": "../../element-plus/es/components/col/style/css.mjs",
|
"src": "../../element-plus/es/components/col/style/css.mjs",
|
||||||
"file": "element-plus_es_components_col_style_css.js",
|
"file": "element-plus_es_components_col_style_css.js",
|
||||||
"fileHash": "2d165654",
|
"fileHash": "bb1da678",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus/es/components/loading/style/css": {
|
"element-plus/es/components/loading/style/css": {
|
||||||
"src": "../../element-plus/es/components/loading/style/css.mjs",
|
"src": "../../element-plus/es/components/loading/style/css.mjs",
|
||||||
"file": "element-plus_es_components_loading_style_css.js",
|
"file": "element-plus_es_components_loading_style_css.js",
|
||||||
"fileHash": "88f7c147",
|
"fileHash": "eef180be",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus/es/components/descriptions/style/css": {
|
"element-plus/es/components/descriptions/style/css": {
|
||||||
"src": "../../element-plus/es/components/descriptions/style/css.mjs",
|
"src": "../../element-plus/es/components/descriptions/style/css.mjs",
|
||||||
"file": "element-plus_es_components_descriptions_style_css.js",
|
"file": "element-plus_es_components_descriptions_style_css.js",
|
||||||
"fileHash": "ce70f737",
|
"fileHash": "4cb57571",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus/es/components/descriptions-item/style/css": {
|
"element-plus/es/components/descriptions-item/style/css": {
|
||||||
"src": "../../element-plus/es/components/descriptions-item/style/css.mjs",
|
"src": "../../element-plus/es/components/descriptions-item/style/css.mjs",
|
||||||
"file": "element-plus_es_components_descriptions-item_style_css.js",
|
"file": "element-plus_es_components_descriptions-item_style_css.js",
|
||||||
"fileHash": "8dd7f2f1",
|
"fileHash": "0f719728",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus/es/components/pagination/style/css": {
|
"element-plus/es/components/pagination/style/css": {
|
||||||
"src": "../../element-plus/es/components/pagination/style/css.mjs",
|
"src": "../../element-plus/es/components/pagination/style/css.mjs",
|
||||||
"file": "element-plus_es_components_pagination_style_css.js",
|
"file": "element-plus_es_components_pagination_style_css.js",
|
||||||
"fileHash": "62918e26",
|
"fileHash": "42e6236a",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus/es/components/table/style/css": {
|
"element-plus/es/components/table/style/css": {
|
||||||
"src": "../../element-plus/es/components/table/style/css.mjs",
|
"src": "../../element-plus/es/components/table/style/css.mjs",
|
||||||
"file": "element-plus_es_components_table_style_css.js",
|
"file": "element-plus_es_components_table_style_css.js",
|
||||||
"fileHash": "d66cccc2",
|
"fileHash": "20763031",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus/es/components/table-column/style/css": {
|
"element-plus/es/components/table-column/style/css": {
|
||||||
"src": "../../element-plus/es/components/table-column/style/css.mjs",
|
"src": "../../element-plus/es/components/table-column/style/css.mjs",
|
||||||
"file": "element-plus_es_components_table-column_style_css.js",
|
"file": "element-plus_es_components_table-column_style_css.js",
|
||||||
"fileHash": "4b9e1b7d",
|
"fileHash": "2b02ba24",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus/es/components/select/style/css": {
|
"element-plus/es/components/select/style/css": {
|
||||||
"src": "../../element-plus/es/components/select/style/css.mjs",
|
"src": "../../element-plus/es/components/select/style/css.mjs",
|
||||||
"file": "element-plus_es_components_select_style_css.js",
|
"file": "element-plus_es_components_select_style_css.js",
|
||||||
"fileHash": "2b082cb1",
|
"fileHash": "06a565f3",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus/es/components/option/style/css": {
|
"element-plus/es/components/option/style/css": {
|
||||||
"src": "../../element-plus/es/components/option/style/css.mjs",
|
"src": "../../element-plus/es/components/option/style/css.mjs",
|
||||||
"file": "element-plus_es_components_option_style_css.js",
|
"file": "element-plus_es_components_option_style_css.js",
|
||||||
"fileHash": "dcd074a6",
|
"fileHash": "0c0e3f42",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus/es/components/switch/style/css": {
|
"element-plus/es/components/switch/style/css": {
|
||||||
"src": "../../element-plus/es/components/switch/style/css.mjs",
|
"src": "../../element-plus/es/components/switch/style/css.mjs",
|
||||||
"file": "element-plus_es_components_switch_style_css.js",
|
"file": "element-plus_es_components_switch_style_css.js",
|
||||||
"fileHash": "eb467983",
|
"fileHash": "b8c8ea9a",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus/es/components/radio-group/style/css": {
|
"element-plus/es/components/radio-group/style/css": {
|
||||||
"src": "../../element-plus/es/components/radio-group/style/css.mjs",
|
"src": "../../element-plus/es/components/radio-group/style/css.mjs",
|
||||||
"file": "element-plus_es_components_radio-group_style_css.js",
|
"file": "element-plus_es_components_radio-group_style_css.js",
|
||||||
"fileHash": "5557d38c",
|
"fileHash": "4174369f",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus/es/components/radio/style/css": {
|
"element-plus/es/components/radio/style/css": {
|
||||||
"src": "../../element-plus/es/components/radio/style/css.mjs",
|
"src": "../../element-plus/es/components/radio/style/css.mjs",
|
||||||
"file": "element-plus_es_components_radio_style_css.js",
|
"file": "element-plus_es_components_radio_style_css.js",
|
||||||
"fileHash": "98bc52ba",
|
"fileHash": "3b71271e",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus/es/components/upload/style/css": {
|
"element-plus/es/components/upload/style/css": {
|
||||||
"src": "../../element-plus/es/components/upload/style/css.mjs",
|
"src": "../../element-plus/es/components/upload/style/css.mjs",
|
||||||
"file": "element-plus_es_components_upload_style_css.js",
|
"file": "element-plus_es_components_upload_style_css.js",
|
||||||
"fileHash": "6b2b7aff",
|
"fileHash": "e7cad837",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus/es/components/image/style/css": {
|
"element-plus/es/components/image/style/css": {
|
||||||
"src": "../../element-plus/es/components/image/style/css.mjs",
|
"src": "../../element-plus/es/components/image/style/css.mjs",
|
||||||
"file": "element-plus_es_components_image_style_css.js",
|
"file": "element-plus_es_components_image_style_css.js",
|
||||||
"fileHash": "7e74c215",
|
"fileHash": "4e0a87a4",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus/es/components/divider/style/css": {
|
"element-plus/es/components/divider/style/css": {
|
||||||
"src": "../../element-plus/es/components/divider/style/css.mjs",
|
"src": "../../element-plus/es/components/divider/style/css.mjs",
|
||||||
"file": "element-plus_es_components_divider_style_css.js",
|
"file": "element-plus_es_components_divider_style_css.js",
|
||||||
"fileHash": "6356af7f",
|
"fileHash": "c846d7f7",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus/es/components/text/style/css": {
|
"element-plus/es/components/text/style/css": {
|
||||||
"src": "../../element-plus/es/components/text/style/css.mjs",
|
"src": "../../element-plus/es/components/text/style/css.mjs",
|
||||||
"file": "element-plus_es_components_text_style_css.js",
|
"file": "element-plus_es_components_text_style_css.js",
|
||||||
"fileHash": "591aafc3",
|
"fileHash": "3e8a90d0",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus/es/components/collapse/style/css": {
|
"element-plus/es/components/collapse/style/css": {
|
||||||
"src": "../../element-plus/es/components/collapse/style/css.mjs",
|
"src": "../../element-plus/es/components/collapse/style/css.mjs",
|
||||||
"file": "element-plus_es_components_collapse_style_css.js",
|
"file": "element-plus_es_components_collapse_style_css.js",
|
||||||
"fileHash": "7310bb21",
|
"fileHash": "51560080",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus/es/components/collapse-item/style/css": {
|
"element-plus/es/components/collapse-item/style/css": {
|
||||||
"src": "../../element-plus/es/components/collapse-item/style/css.mjs",
|
"src": "../../element-plus/es/components/collapse-item/style/css.mjs",
|
||||||
"file": "element-plus_es_components_collapse-item_style_css.js",
|
"file": "element-plus_es_components_collapse-item_style_css.js",
|
||||||
"fileHash": "27e07e55",
|
"fileHash": "94bca0a2",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
|
},
|
||||||
|
"qrcode": {
|
||||||
|
"src": "../../qrcode/lib/browser.js",
|
||||||
|
"file": "qrcode.js",
|
||||||
|
"fileHash": "ad0f2956",
|
||||||
|
"needsInterop": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"chunks": {
|
"chunks": {
|
||||||
@ -321,6 +327,9 @@
|
|||||||
"chunk-5KK3TTMN": {
|
"chunk-5KK3TTMN": {
|
||||||
"file": "chunk-5KK3TTMN.js"
|
"file": "chunk-5KK3TTMN.js"
|
||||||
},
|
},
|
||||||
|
"chunk-NKQWFVTF": {
|
||||||
|
"file": "chunk-NKQWFVTF.js"
|
||||||
|
},
|
||||||
"chunk-REWOA3VH": {
|
"chunk-REWOA3VH": {
|
||||||
"file": "chunk-REWOA3VH.js"
|
"file": "chunk-REWOA3VH.js"
|
||||||
},
|
},
|
||||||
@ -330,9 +339,6 @@
|
|||||||
"chunk-SMFPDFTD": {
|
"chunk-SMFPDFTD": {
|
||||||
"file": "chunk-SMFPDFTD.js"
|
"file": "chunk-SMFPDFTD.js"
|
||||||
},
|
},
|
||||||
"chunk-NKQWFVTF": {
|
|
||||||
"file": "chunk-NKQWFVTF.js"
|
|
||||||
},
|
|
||||||
"chunk-IV6PSERC": {
|
"chunk-IV6PSERC": {
|
||||||
"file": "chunk-IV6PSERC.js"
|
"file": "chunk-IV6PSERC.js"
|
||||||
},
|
},
|
||||||
|
|||||||
2
admin/node_modules/.vite/deps/element-plus_es_components_pagination_style_css.js
generated
vendored
@ -1,9 +1,9 @@
|
|||||||
import "./chunk-75C4BP7B.js";
|
import "./chunk-75C4BP7B.js";
|
||||||
import "./chunk-UBLR4G7Q.js";
|
import "./chunk-UBLR4G7Q.js";
|
||||||
import "./chunk-5KK3TTMN.js";
|
import "./chunk-5KK3TTMN.js";
|
||||||
|
import "./chunk-NKQWFVTF.js";
|
||||||
import "./chunk-REWOA3VH.js";
|
import "./chunk-REWOA3VH.js";
|
||||||
import "./chunk-TX5YLZ4O.js";
|
import "./chunk-TX5YLZ4O.js";
|
||||||
import "./chunk-NKQWFVTF.js";
|
|
||||||
import "./chunk-IV6PSERC.js";
|
import "./chunk-IV6PSERC.js";
|
||||||
|
|
||||||
// node_modules/element-plus/es/components/pagination/style/css.mjs
|
// node_modules/element-plus/es/components/pagination/style/css.mjs
|
||||||
|
|||||||
2083
admin/node_modules/.vite/deps/qrcode.js
generated
vendored
Normal file
7
admin/node_modules/.vite/deps/qrcode.js.map
generated
vendored
Normal file
2
admin/package-lock.json
generated
@ -14,7 +14,7 @@
|
|||||||
"echarts": "^5.4.3",
|
"echarts": "^5.4.3",
|
||||||
"element-plus": "^2.4.4",
|
"element-plus": "^2.4.4",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"qrcode": "^1.5.3",
|
"qrcode": "^1.5.4",
|
||||||
"vue": "^3.4.0",
|
"vue": "^3.4.0",
|
||||||
"vue-router": "^4.2.5"
|
"vue-router": "^4.2.5"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -9,21 +9,21 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"vue": "^3.4.0",
|
|
||||||
"vue-router": "^4.2.5",
|
|
||||||
"pinia": "^2.1.7",
|
|
||||||
"element-plus": "^2.4.4",
|
|
||||||
"@element-plus/icons-vue": "^2.3.1",
|
"@element-plus/icons-vue": "^2.3.1",
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
|
"dayjs": "^1.11.10",
|
||||||
"echarts": "^5.4.3",
|
"echarts": "^5.4.3",
|
||||||
"qrcode": "^1.5.3",
|
"element-plus": "^2.4.4",
|
||||||
"dayjs": "^1.11.10"
|
"pinia": "^2.1.7",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
|
"vue": "^3.4.0",
|
||||||
|
"vue-router": "^4.2.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^4.5.2",
|
"@vitejs/plugin-vue": "^4.5.2",
|
||||||
"vite": "^5.0.10",
|
|
||||||
"sass": "^1.69.5",
|
"sass": "^1.69.5",
|
||||||
"unplugin-auto-import": "^0.17.2",
|
"unplugin-auto-import": "^0.17.2",
|
||||||
"unplugin-vue-components": "^0.26.0"
|
"unplugin-vue-components": "^0.26.0",
|
||||||
|
"vite": "^5.0.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,7 +38,14 @@
|
|||||||
<!-- 表格 -->
|
<!-- 表格 -->
|
||||||
<el-table :data="tableData" v-loading="loading" stripe>
|
<el-table :data="tableData" v-loading="loading" stripe>
|
||||||
<el-table-column prop="id" label="ID" width="80" />
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
<el-table-column prop="matchCode" label="比赛码" width="140" />
|
<el-table-column prop="matchCode" label="比赛码" width="160">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="match-code-cell" @click="showQrcode(row)">
|
||||||
|
<span class="code">{{ row.matchCode }}</span>
|
||||||
|
<el-icon class="qr-icon"><View /></el-icon>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="name" label="比赛名称" min-width="180" />
|
<el-table-column prop="name" label="比赛名称" min-width="180" />
|
||||||
<el-table-column v-if="userStore.isSuperAdmin" prop="storeName" label="门店" width="140" />
|
<el-table-column v-if="userStore.isSuperAdmin" prop="storeName" label="门店" width="140" />
|
||||||
<el-table-column prop="type" label="类型" width="100">
|
<el-table-column prop="type" label="类型" width="100">
|
||||||
@ -110,14 +117,38 @@
|
|||||||
<el-button type="primary" @click="handleCreate">创建</el-button>
|
<el-button type="primary" @click="handleCreate">创建</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 比赛二维码弹窗 -->
|
||||||
|
<el-dialog v-model="showQrcodeDialog" title="比赛二维码" width="400px" class="qrcode-dialog">
|
||||||
|
<div class="qrcode-content">
|
||||||
|
<div class="qrcode-box" ref="qrcodeRef"></div>
|
||||||
|
<div class="match-info">
|
||||||
|
<div class="match-name">{{ currentMatch?.name }}</div>
|
||||||
|
<div class="match-code">{{ currentMatch?.matchCode }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="qrcode-tip">用户扫描二维码即可加入比赛</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="downloadQrcode">
|
||||||
|
<el-icon><Download /></el-icon>
|
||||||
|
下载二维码
|
||||||
|
</el-button>
|
||||||
|
<el-button type="primary" @click="printQrcode">
|
||||||
|
<el-icon><Printer /></el-icon>
|
||||||
|
打印
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, nextTick } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { View, Download, Printer } from '@element-plus/icons-vue'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
import QRCode from 'qrcode'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import { getMatches, createMatch, startMatch, endMatch, getStores } from '@/api/admin'
|
import { getMatches, createMatch, startMatch, endMatch, getStores } from '@/api/admin'
|
||||||
|
|
||||||
@ -130,6 +161,12 @@ const stores = ref([])
|
|||||||
const searchForm = ref({ store_id: '', type: '', status: '' })
|
const searchForm = ref({ store_id: '', type: '', status: '' })
|
||||||
const pagination = ref({ page: 1, pageSize: 20, total: 0 })
|
const pagination = ref({ page: 1, pageSize: 20, total: 0 })
|
||||||
|
|
||||||
|
// 二维码相关
|
||||||
|
const showQrcodeDialog = ref(false)
|
||||||
|
const currentMatch = ref(null)
|
||||||
|
const qrcodeRef = ref()
|
||||||
|
const qrcodeDataUrl = ref('')
|
||||||
|
|
||||||
const showCreateDialog = ref(false)
|
const showCreateDialog = ref(false)
|
||||||
const createFormRef = ref()
|
const createFormRef = ref()
|
||||||
const createForm = ref({
|
const createForm = ref({
|
||||||
@ -224,8 +261,159 @@ const handleEnd = (row) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 显示比赛二维码
|
||||||
|
const showQrcode = async (row) => {
|
||||||
|
currentMatch.value = row
|
||||||
|
showQrcodeDialog.value = true
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
// 生成二维码
|
||||||
|
try {
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
await QRCode.toCanvas(canvas, row.matchCode, {
|
||||||
|
width: 200,
|
||||||
|
margin: 2,
|
||||||
|
color: {
|
||||||
|
dark: '#1A1A2E',
|
||||||
|
light: '#FFFFFF'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 清空容器并添加 canvas
|
||||||
|
if (qrcodeRef.value) {
|
||||||
|
qrcodeRef.value.innerHTML = ''
|
||||||
|
qrcodeRef.value.appendChild(canvas)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存为 dataUrl 用于下载
|
||||||
|
qrcodeDataUrl.value = canvas.toDataURL('image/png')
|
||||||
|
} catch (err) {
|
||||||
|
console.error('生成二维码失败:', err)
|
||||||
|
ElMessage.error('生成二维码失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载二维码
|
||||||
|
const downloadQrcode = () => {
|
||||||
|
if (!qrcodeDataUrl.value) return
|
||||||
|
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.download = `比赛二维码_${currentMatch.value?.matchCode}.png`
|
||||||
|
link.href = qrcodeDataUrl.value
|
||||||
|
link.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打印二维码
|
||||||
|
const printQrcode = () => {
|
||||||
|
const printWindow = window.open('', '_blank')
|
||||||
|
if (!printWindow) {
|
||||||
|
ElMessage.warning('请允许弹出窗口以打印')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
printWindow.document.write(`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>比赛二维码</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
.qrcode { margin-bottom: 20px; }
|
||||||
|
.match-name { font-size: 24px; font-weight: bold; margin-bottom: 10px; }
|
||||||
|
.match-code { font-size: 20px; color: #FF6B35; margin-bottom: 10px; }
|
||||||
|
.tip { font-size: 14px; color: #666; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<img class="qrcode" src="${qrcodeDataUrl.value}" width="200" height="200" />
|
||||||
|
<div class="match-name">${currentMatch.value?.name}</div>
|
||||||
|
<div class="match-code">${currentMatch.value?.matchCode}</div>
|
||||||
|
<div class="tip">扫描二维码加入比赛</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`)
|
||||||
|
printWindow.document.close()
|
||||||
|
printWindow.focus()
|
||||||
|
printWindow.print()
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchStores()
|
fetchStores()
|
||||||
fetchData()
|
fetchData()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.match-code-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--primary-color);
|
||||||
|
|
||||||
|
.qr-icon {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.code {
|
||||||
|
font-family: monospace;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-icon {
|
||||||
|
margin-left: 6px;
|
||||||
|
opacity: 0.5;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.qrcode-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px 0;
|
||||||
|
|
||||||
|
.qrcode-box {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-info {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.match-name {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-code {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.qrcode-tip {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@ -1,212 +1,290 @@
|
|||||||
|
const config = require("./config");
|
||||||
|
|
||||||
App({
|
App({
|
||||||
globalData: {
|
globalData: {
|
||||||
userInfo: null,
|
userInfo: null,
|
||||||
token: null,
|
token: null,
|
||||||
currentStore: null,
|
currentStore: null,
|
||||||
ladderUser: null,
|
ladderUser: null,
|
||||||
wsConnected: false
|
wsConnected: false,
|
||||||
|
// 微信登录临时信息
|
||||||
|
wxLoginInfo: null,
|
||||||
|
// 从配置文件读取
|
||||||
|
baseUrl: config.baseUrl,
|
||||||
|
wsUrl: config.wsUrl,
|
||||||
},
|
},
|
||||||
|
|
||||||
onLaunch() {
|
onLaunch() {
|
||||||
// 从本地存储读取token
|
// 从本地存储读取token
|
||||||
const token = wx.getStorageSync('token')
|
const token = wx.getStorageSync("token");
|
||||||
if (token) {
|
if (token) {
|
||||||
this.globalData.token = token
|
this.globalData.token = token;
|
||||||
this.getUserInfo()
|
this.getUserInfo();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 登录
|
// 微信登录(第一步:获取openid和session_key)
|
||||||
login() {
|
wxLogin() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
wx.login({
|
wx.login({
|
||||||
success: res => {
|
success: (res) => {
|
||||||
wx.request({
|
wx.request({
|
||||||
url: `${this.globalData.baseUrl}/api/user/login`,
|
url: `${this.globalData.baseUrl}/api/user/login`,
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
data: { code: res.code },
|
data: { code: res.code },
|
||||||
success: loginRes => {
|
success: (loginRes) => {
|
||||||
if (loginRes.data.code === 0) {
|
if (loginRes.data.code === 0) {
|
||||||
this.globalData.token = loginRes.data.data.token
|
const data = loginRes.data.data;
|
||||||
this.globalData.userInfo = loginRes.data.data.userInfo
|
// 保存微信登录信息(用于后续手机号授权)
|
||||||
wx.setStorageSync('token', loginRes.data.data.token)
|
this.globalData.wxLoginInfo = {
|
||||||
this.connectWebSocket()
|
openid: data.openid,
|
||||||
resolve(loginRes.data.data)
|
unionid: data.unionid,
|
||||||
|
sessionKey: data.sessionKey,
|
||||||
|
isNewUser: data.isNewUser,
|
||||||
|
hasPhone: data.hasPhone,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 如果已有token(老用户),直接使用
|
||||||
|
if (data.userInfo && data.hasPhone) {
|
||||||
|
// 老用户已绑定手机号,生成token并登录
|
||||||
|
this.globalData.userInfo = data.userInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(data);
|
||||||
} else {
|
} else {
|
||||||
reject(loginRes.data)
|
reject(loginRes.data);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fail: reject
|
fail: reject,
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
fail: reject
|
fail: reject,
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 手机号授权登录(第二步:解密手机号完成注册/登录)
|
||||||
|
phoneLogin(encryptedData, iv, userProfile) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const wxInfo = this.globalData.wxLoginInfo;
|
||||||
|
if (!wxInfo) {
|
||||||
|
reject({ message: "请先进行微信登录" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
wx.request({
|
||||||
|
url: `${this.globalData.baseUrl}/api/user/phone-login`,
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
openid: wxInfo.openid,
|
||||||
|
unionid: wxInfo.unionid,
|
||||||
|
sessionKey: wxInfo.sessionKey,
|
||||||
|
encryptedData,
|
||||||
|
iv,
|
||||||
|
nickname: userProfile?.nickName || "",
|
||||||
|
avatar: userProfile?.avatarUrl || "",
|
||||||
|
gender: userProfile?.gender || 0,
|
||||||
|
},
|
||||||
|
success: (loginRes) => {
|
||||||
|
if (loginRes.data.code === 0) {
|
||||||
|
this.globalData.token = loginRes.data.data.token;
|
||||||
|
this.globalData.userInfo = loginRes.data.data.userInfo;
|
||||||
|
wx.setStorageSync("token", loginRes.data.data.token);
|
||||||
|
this.connectWebSocket();
|
||||||
|
resolve(loginRes.data.data);
|
||||||
|
} else {
|
||||||
|
reject(loginRes.data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: reject,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 旧的登录方法(兼容)
|
||||||
|
login() {
|
||||||
|
return this.wxLogin();
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取用户信息
|
// 获取用户信息
|
||||||
getUserInfo() {
|
getUserInfo() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.request('/api/user/info').then(res => {
|
this.request("/api/user/info")
|
||||||
this.globalData.userInfo = res.data
|
.then((res) => {
|
||||||
this.connectWebSocket()
|
this.globalData.userInfo = res.data;
|
||||||
resolve(res.data)
|
this.connectWebSocket();
|
||||||
}).catch(reject)
|
resolve(res.data);
|
||||||
})
|
})
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取当前门店
|
// 获取当前门店
|
||||||
getCurrentStore() {
|
getCurrentStore() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
wx.getLocation({
|
wx.getLocation({
|
||||||
type: 'gcj02',
|
type: "gcj02",
|
||||||
success: loc => {
|
success: (loc) => {
|
||||||
this.request('/api/user/current-store', {
|
this.request("/api/user/current-store", {
|
||||||
latitude: loc.latitude,
|
latitude: loc.latitude,
|
||||||
longitude: loc.longitude
|
longitude: loc.longitude,
|
||||||
}).then(res => {
|
})
|
||||||
this.globalData.currentStore = res.data
|
.then((res) => {
|
||||||
|
this.globalData.currentStore = res.data;
|
||||||
if (res.data?.ladderUserId) {
|
if (res.data?.ladderUserId) {
|
||||||
this.getLadderUser(res.data.storeId)
|
this.getLadderUser(res.data.storeId);
|
||||||
}
|
}
|
||||||
resolve(res.data)
|
resolve(res.data);
|
||||||
}).catch(reject)
|
})
|
||||||
|
.catch(reject);
|
||||||
},
|
},
|
||||||
fail: () => {
|
fail: () => {
|
||||||
// 无法获取位置,使用默认门店
|
// 无法获取位置,使用默认门店
|
||||||
this.request('/api/user/current-store').then(res => {
|
this.request("/api/user/current-store")
|
||||||
this.globalData.currentStore = res.data
|
.then((res) => {
|
||||||
resolve(res.data)
|
this.globalData.currentStore = res.data;
|
||||||
}).catch(reject)
|
resolve(res.data);
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
.catch(reject);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取天梯用户信息
|
// 获取天梯用户信息
|
||||||
getLadderUser(storeId) {
|
getLadderUser(storeId) {
|
||||||
return this.request('/api/user/ladder-info', { store_id: storeId }).then(res => {
|
return this.request("/api/user/ladder-info", { store_id: storeId }).then(
|
||||||
|
(res) => {
|
||||||
if (res.data && res.data.length > 0) {
|
if (res.data && res.data.length > 0) {
|
||||||
this.globalData.ladderUser = res.data[0]
|
this.globalData.ladderUser = res.data[0];
|
||||||
}
|
}
|
||||||
return res.data
|
return res.data;
|
||||||
})
|
}
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
// WebSocket连接
|
// WebSocket连接
|
||||||
connectWebSocket() {
|
connectWebSocket() {
|
||||||
if (this.globalData.wsConnected || !this.globalData.token) return
|
if (this.globalData.wsConnected || !this.globalData.token) return;
|
||||||
|
|
||||||
const wsUrl = this.globalData.wsUrl || 'ws://localhost:3000/ws'
|
const wsUrl = this.globalData.wsUrl || "ws://localhost:3000/ws";
|
||||||
|
|
||||||
this.ws = wx.connectSocket({
|
this.ws = wx.connectSocket({
|
||||||
url: wsUrl,
|
url: wsUrl,
|
||||||
success: () => {
|
success: () => {
|
||||||
console.log('WebSocket连接中...')
|
console.log("WebSocket连接中...");
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
wx.onSocketOpen(() => {
|
wx.onSocketOpen(() => {
|
||||||
console.log('WebSocket已连接')
|
console.log("WebSocket已连接");
|
||||||
this.globalData.wsConnected = true
|
this.globalData.wsConnected = true;
|
||||||
// 发送认证
|
// 发送认证
|
||||||
wx.sendSocketMessage({
|
wx.sendSocketMessage({
|
||||||
data: JSON.stringify({
|
data: JSON.stringify({
|
||||||
type: 'auth',
|
type: "auth",
|
||||||
token: this.globalData.token
|
token: this.globalData.token,
|
||||||
})
|
}),
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
wx.onSocketMessage(res => {
|
wx.onSocketMessage((res) => {
|
||||||
const data = JSON.parse(res.data)
|
const data = JSON.parse(res.data);
|
||||||
this.handleWsMessage(data)
|
this.handleWsMessage(data);
|
||||||
})
|
});
|
||||||
|
|
||||||
wx.onSocketClose(() => {
|
wx.onSocketClose(() => {
|
||||||
console.log('WebSocket已断开')
|
console.log("WebSocket已断开");
|
||||||
this.globalData.wsConnected = false
|
this.globalData.wsConnected = false;
|
||||||
// 尝试重连
|
// 尝试重连
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.connectWebSocket()
|
this.connectWebSocket();
|
||||||
}, 5000)
|
}, 5000);
|
||||||
})
|
});
|
||||||
|
|
||||||
wx.onSocketError(err => {
|
wx.onSocketError((err) => {
|
||||||
console.error('WebSocket错误:', err)
|
console.error("WebSocket错误:", err);
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// 处理WebSocket消息
|
// 处理WebSocket消息
|
||||||
handleWsMessage(data) {
|
handleWsMessage(data) {
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
case 'challenge_request':
|
case "challenge_request":
|
||||||
// 收到挑战请求
|
// 收到挑战请求
|
||||||
wx.showModal({
|
wx.showModal({
|
||||||
title: '收到挑战',
|
title: "收到挑战",
|
||||||
content: `${data.data.challenger.realName} 向你发起挑战`,
|
content: `${data.data.challenger.realName} 向你发起挑战`,
|
||||||
confirmText: '接受',
|
confirmText: "接受",
|
||||||
cancelText: '拒绝',
|
cancelText: "拒绝",
|
||||||
success: res => {
|
success: (res) => {
|
||||||
this.request('/api/match/challenge/respond', {
|
this.request(
|
||||||
|
"/api/match/challenge/respond",
|
||||||
|
{
|
||||||
match_id: data.data.matchId,
|
match_id: data.data.matchId,
|
||||||
accept: res.confirm
|
accept: res.confirm,
|
||||||
}, 'POST')
|
},
|
||||||
}
|
"POST"
|
||||||
})
|
);
|
||||||
break
|
},
|
||||||
case 'score_confirm_request':
|
});
|
||||||
|
break;
|
||||||
|
case "score_confirm_request":
|
||||||
// 收到比分确认请求
|
// 收到比分确认请求
|
||||||
wx.showModal({
|
wx.showModal({
|
||||||
title: '确认比分',
|
title: "确认比分",
|
||||||
content: `比分: ${data.data.player1Score} : ${data.data.player2Score}`,
|
content: `比分: ${data.data.player1Score} : ${data.data.player2Score}`,
|
||||||
confirmText: '确认',
|
confirmText: "确认",
|
||||||
cancelText: '有争议',
|
cancelText: "有争议",
|
||||||
success: res => {
|
success: (res) => {
|
||||||
this.request('/api/match/challenge/confirm-score', {
|
this.request(
|
||||||
|
"/api/match/challenge/confirm-score",
|
||||||
|
{
|
||||||
game_id: data.data.gameId,
|
game_id: data.data.gameId,
|
||||||
confirm: res.confirm
|
confirm: res.confirm,
|
||||||
}, 'POST')
|
},
|
||||||
}
|
"POST"
|
||||||
})
|
);
|
||||||
break
|
},
|
||||||
case 'match_paired':
|
});
|
||||||
|
break;
|
||||||
|
case "match_paired":
|
||||||
// 排位赛匹配通知
|
// 排位赛匹配通知
|
||||||
wx.showModal({
|
wx.showModal({
|
||||||
title: '匹配成功',
|
title: "匹配成功",
|
||||||
content: `你的对手是: ${data.data.opponent.realName}`,
|
content: `你的对手是: ${data.data.opponent.realName}`,
|
||||||
showCancel: false
|
showCancel: false,
|
||||||
})
|
});
|
||||||
break
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 封装请求
|
// 封装请求
|
||||||
request(url, data = {}, method = 'GET') {
|
request(url, data = {}, method = "GET") {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
wx.request({
|
wx.request({
|
||||||
url: `${this.globalData.baseUrl}${url}`,
|
url: `${this.globalData.baseUrl}${url}`,
|
||||||
method,
|
method,
|
||||||
data,
|
data,
|
||||||
header: {
|
header: {
|
||||||
'Authorization': `Bearer ${this.globalData.token}`
|
Authorization: `Bearer ${this.globalData.token}`,
|
||||||
},
|
},
|
||||||
success: res => {
|
success: (res) => {
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
resolve(res.data)
|
resolve(res.data);
|
||||||
} else if (res.data.code === 401) {
|
} else if (res.data.code === 401) {
|
||||||
// 登录过期
|
// 登录过期
|
||||||
this.globalData.token = null
|
this.globalData.token = null;
|
||||||
wx.removeStorageSync('token')
|
wx.removeStorageSync("token");
|
||||||
wx.reLaunch({ url: '/pages/user/index' })
|
wx.reLaunch({ url: "/pages/user/index" });
|
||||||
reject(res.data)
|
reject(res.data);
|
||||||
} else {
|
} else {
|
||||||
wx.showToast({ title: res.data.message, icon: 'none' })
|
wx.showToast({ title: res.data.message, icon: "none" });
|
||||||
reject(res.data)
|
reject(res.data);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fail: reject
|
fail: reject,
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|||||||
48
miniprogram/config.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* 小程序配置文件
|
||||||
|
* 请根据实际环境修改以下配置
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 开发环境配置
|
||||||
|
const devConfig = {
|
||||||
|
// API 基础地址(本地开发)
|
||||||
|
baseUrl: "http://localhost:3000",
|
||||||
|
// WebSocket 地址(本地开发)
|
||||||
|
wsUrl: "ws://localhost:3000/ws",
|
||||||
|
};
|
||||||
|
|
||||||
|
// 生产环境配置
|
||||||
|
const prodConfig = {
|
||||||
|
// API 基础地址(生产环境,请替换为实际域名)
|
||||||
|
baseUrl: "https://your-domain.com",
|
||||||
|
// WebSocket 地址(生产环境,请替换为实际域名)
|
||||||
|
wsUrl: "wss://your-domain.com/ws",
|
||||||
|
};
|
||||||
|
|
||||||
|
// 根据环境变量选择配置
|
||||||
|
// 小程序可以通过 __wxConfig.envVersion 获取当前环境
|
||||||
|
// develop: 开发版, trial: 体验版, release: 正式版
|
||||||
|
const getEnv = () => {
|
||||||
|
try {
|
||||||
|
// 尝试获取微信环境
|
||||||
|
const envVersion = __wxConfig?.envVersion || "develop";
|
||||||
|
return envVersion === "release" ? "production" : "development";
|
||||||
|
} catch (e) {
|
||||||
|
return "development";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const env = getEnv();
|
||||||
|
const config = env === "production" ? prodConfig : devConfig;
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
...config,
|
||||||
|
env,
|
||||||
|
// 其他配置项
|
||||||
|
// 上传文件大小限制 (MB)
|
||||||
|
uploadMaxSize: 5,
|
||||||
|
// 请求超时时间 (ms)
|
||||||
|
requestTimeout: 30000,
|
||||||
|
// 版本号
|
||||||
|
version: "1.0.0",
|
||||||
|
};
|
||||||
40
miniprogram/env-template.txt
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
================================================================================
|
||||||
|
影沙俱乐部小程序配置说明
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
【配置文件位置】
|
||||||
|
config.js
|
||||||
|
|
||||||
|
【开发环境配置】
|
||||||
|
在 config.js 中修改 devConfig:
|
||||||
|
- baseUrl: API 服务器地址,本地开发默认 http://localhost:3000
|
||||||
|
- wsUrl: WebSocket 地址,本地开发默认 ws://localhost:3000/ws
|
||||||
|
|
||||||
|
【生产环境配置】
|
||||||
|
在 config.js 中修改 prodConfig:
|
||||||
|
- baseUrl: 正式环境 API 地址,如 https://api.yingsha.com
|
||||||
|
- wsUrl: 正式环境 WebSocket 地址,如 wss://api.yingsha.com/ws
|
||||||
|
|
||||||
|
【微信小程序后台配置】
|
||||||
|
1. 登录微信公众平台 -> 开发管理 -> 开发设置
|
||||||
|
2. 服务器域名配置:
|
||||||
|
- request 合法域名: 添加你的 API 域名(https://)
|
||||||
|
- socket 合法域名: 添加你的 WebSocket 域名(wss://)
|
||||||
|
- uploadFile 合法域名: 添加你的上传文件域名
|
||||||
|
- downloadFile 合法域名: 添加你的下载文件域名
|
||||||
|
|
||||||
|
3. 业务域名配置(如需要 webview):
|
||||||
|
- 添加需要在 webview 中打开的域名
|
||||||
|
|
||||||
|
【本地开发调试】
|
||||||
|
1. 微信开发者工具中勾选"不校验合法域名"
|
||||||
|
2. 确保本地服务器已启动:
|
||||||
|
cd server
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
【注意事项】
|
||||||
|
- 正式环境必须使用 HTTPS 和 WSS
|
||||||
|
- 配置更改后需要重新编译小程序
|
||||||
|
- 首次发布需要在微信后台配置服务器域名
|
||||||
|
|
||||||
|
================================================================================
|
||||||
5
miniprogram/images/avatar-default.svg
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
|
||||||
|
<rect width="120" height="120" fill="#e8e8e8"/>
|
||||||
|
<circle cx="60" cy="45" r="25" fill="#bfbfbf"/>
|
||||||
|
<ellipse cx="60" cy="100" rx="40" ry="30" fill="#bfbfbf"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 263 B |
7
miniprogram/images/empty-match.svg
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 160" width="200" height="160">
|
||||||
|
<circle cx="70" cy="60" r="25" fill="#e8e8e8"/>
|
||||||
|
<circle cx="130" cy="60" r="25" fill="#e8e8e8"/>
|
||||||
|
<text x="100" y="68" text-anchor="middle" fill="#d9d9d9" font-size="24">VS</text>
|
||||||
|
<rect x="50" y="100" width="100" height="20" fill="#e8e8e8" rx="4"/>
|
||||||
|
<text x="100" y="150" text-anchor="middle" fill="#bfbfbf" font-size="14">暂无比赛记录</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 463 B |
8
miniprogram/images/empty-order.svg
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 160" width="200" height="160">
|
||||||
|
<rect x="50" y="25" width="100" height="100" fill="#e8e8e8" rx="8"/>
|
||||||
|
<rect x="65" y="40" width="70" height="35" fill="#d9d9d9" rx="4"/>
|
||||||
|
<rect x="65" y="85" width="45" height="10" fill="#f5f5f5" rx="2"/>
|
||||||
|
<rect x="65" y="100" width="30" height="15" fill="#faad14" rx="3"/>
|
||||||
|
<circle cx="130" cy="107" r="8" fill="#d9d9d9"/>
|
||||||
|
<text x="100" y="145" text-anchor="middle" fill="#bfbfbf" font-size="14">暂无订单</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 533 B |
11
miniprogram/images/empty-products.svg
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 160" width="200" height="160">
|
||||||
|
<rect x="40" y="30" width="50" height="70" fill="#e8e8e8" rx="6"/>
|
||||||
|
<rect x="110" y="30" width="50" height="70" fill="#e8e8e8" rx="6"/>
|
||||||
|
<rect x="50" y="40" width="30" height="30" fill="#d9d9d9" rx="4"/>
|
||||||
|
<rect x="120" y="40" width="30" height="30" fill="#d9d9d9" rx="4"/>
|
||||||
|
<rect x="50" y="75" width="30" height="6" fill="#f5f5f5" rx="2"/>
|
||||||
|
<rect x="120" y="75" width="30" height="6" fill="#f5f5f5" rx="2"/>
|
||||||
|
<rect x="50" y="85" width="20" height="8" fill="#faad14" rx="2"/>
|
||||||
|
<rect x="120" y="85" width="20" height="8" fill="#faad14" rx="2"/>
|
||||||
|
<text x="100" y="130" text-anchor="middle" fill="#bfbfbf" font-size="14">暂无商品</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 761 B |
9
miniprogram/images/empty-ranking.svg
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 160" width="200" height="160">
|
||||||
|
<rect x="40" y="80" width="30" height="50" fill="#e8e8e8" rx="4"/>
|
||||||
|
<rect x="85" y="50" width="30" height="80" fill="#e8e8e8" rx="4"/>
|
||||||
|
<rect x="130" y="95" width="30" height="35" fill="#e8e8e8" rx="4"/>
|
||||||
|
<circle cx="55" cy="65" r="12" fill="#d9d9d9"/>
|
||||||
|
<circle cx="100" cy="35" r="12" fill="#d9d9d9"/>
|
||||||
|
<circle cx="145" cy="80" r="12" fill="#d9d9d9"/>
|
||||||
|
<text x="100" y="150" text-anchor="middle" fill="#bfbfbf" font-size="14">暂无排名数据</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 571 B |
9
miniprogram/images/empty-records.svg
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 160" width="200" height="160">
|
||||||
|
<rect x="50" y="30" width="100" height="90" fill="#e8e8e8" rx="8"/>
|
||||||
|
<rect x="65" y="45" width="70" height="8" fill="#d9d9d9" rx="2"/>
|
||||||
|
<rect x="65" y="60" width="50" height="8" fill="#f5f5f5" rx="2"/>
|
||||||
|
<rect x="65" y="75" width="60" height="8" fill="#d9d9d9" rx="2"/>
|
||||||
|
<rect x="65" y="90" width="40" height="8" fill="#f5f5f5" rx="2"/>
|
||||||
|
<rect x="65" y="105" width="55" height="8" fill="#d9d9d9" rx="2"/>
|
||||||
|
<text x="100" y="145" text-anchor="middle" fill="#bfbfbf" font-size="14">暂无记录</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 616 B |
8
miniprogram/images/empty-store.svg
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 160" width="200" height="160">
|
||||||
|
<rect x="50" y="40" width="100" height="70" fill="#e8e8e8" rx="8"/>
|
||||||
|
<rect x="60" y="50" width="25" height="20" fill="#d9d9d9" rx="2"/>
|
||||||
|
<rect x="90" y="50" width="50" height="10" fill="#d9d9d9" rx="2"/>
|
||||||
|
<rect x="90" y="65" width="40" height="8" fill="#f5f5f5" rx="2"/>
|
||||||
|
<path d="M70 85 L80 95 L130 95 L130 110 L80 110 Z" fill="#d9d9d9"/>
|
||||||
|
<text x="100" y="140" text-anchor="middle" fill="#bfbfbf" font-size="14">暂无门店</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 549 B |
3
miniprogram/images/icon-arrow.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||||
|
<path fill="#999999" d="M18 12l2.83-2.83L32.66 21 20.83 32.83 18 30l9-9z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 172 B |
3
miniprogram/images/icon-challenge.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||||
|
<path fill="#ff6b35" d="M24 4l5.5 11.5L42 17l-9 9 2 12.5L24 33l-11 5.5 2-12.5-9-9 12.5-1.5z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 191 B |
4
miniprogram/images/icon-check.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||||
|
<circle cx="24" cy="24" r="20" fill="#52c41a"/>
|
||||||
|
<path fill="#ffffff" d="M20 30.59l-6.29-6.3 2.12-2.12L20 26.34l12.17-12.17 2.12 2.12z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 237 B |
3
miniprogram/images/icon-history.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||||
|
<path fill="#666666" d="M25.99 6C16.04 6 8 14.06 8 24H2l7.79 7.79.14.29L18 24h-6c0-7.73 6.27-14 14-14s14 6.27 14 14-6.27 14-14 14c-3.87 0-7.36-1.58-9.89-4.11l-2.83 2.83C16.53 39.98 21.02 42 26 42c9.94 0 18-8.06 18-18S35.94 6 25.99 6zM24 16v10l8.56 5.08 1.44-2.43-7-4.15V16h-3z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 376 B |
4
miniprogram/images/icon-info.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||||
|
<circle cx="24" cy="24" r="20" fill="#1890ff"/>
|
||||||
|
<text x="24" y="32" text-anchor="middle" fill="#ffffff" font-size="24" font-weight="bold">i</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 247 B |
3
miniprogram/images/icon-order.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||||
|
<path fill="#666666" d="M38 6H10c-2.21 0-4 1.79-4 4v28c0 2.21 1.79 4 4 4h28c2.21 0 4-1.79 4-4V10c0-2.21-1.79-4-4-4zm-22 6h16v4H16v-4zm16 20H16v-4h16v4zm0-8H16v-4h16v4z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 267 B |
4
miniprogram/images/icon-points.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||||
|
<circle cx="24" cy="24" r="18" fill="#faad14"/>
|
||||||
|
<text x="24" y="30" text-anchor="middle" fill="#ffffff" font-size="18" font-weight="bold">P</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 247 B |
3
miniprogram/images/icon-qrcode.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||||
|
<path fill="#666666" d="M8 8h14v14H8V8zm4 4v6h6v-6h-6zm12-4h14v14H24V8zm4 4v6h6v-6h-6zM8 26h14v14H8V26zm4 4v6h6v-6h-6zm16-4h4v4h-4zm4 4h4v4h-4zm-4 4h4v4h-4zm4 4h4v4h-4zm4-8h4v4h-4zm0 8h4v4h-4z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 292 B |
8
miniprogram/images/icon-ranking.svg
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||||
|
<rect x="6" y="26" width="10" height="16" fill="#52c41a"/>
|
||||||
|
<rect x="19" y="14" width="10" height="28" fill="#faad14"/>
|
||||||
|
<rect x="32" y="20" width="10" height="22" fill="#1890ff"/>
|
||||||
|
<text x="11" y="24" text-anchor="middle" fill="#666" font-size="10">2</text>
|
||||||
|
<text x="24" y="12" text-anchor="middle" fill="#666" font-size="10">1</text>
|
||||||
|
<text x="37" y="18" text-anchor="middle" fill="#666" font-size="10">3</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 526 B |
3
miniprogram/images/icon-records.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||||
|
<path fill="#666666" d="M14 6c-2.21 0-4 1.79-4 4v28c0 2.21 1.79 4 4 4h20c2.21 0 4-1.79 4-4V10c0-2.21-1.79-4-4-4H14zm2 8h16v4H16v-4zm0 8h12v4H16v-4zm0 8h16v4H16v-4z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 263 B |
3
miniprogram/images/icon-scan.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||||
|
<path fill="#666666" d="M6 14V8c0-1.1.9-2 2-2h6v4H10v4H6zm32-8h6c1.1 0 2 .9 2 2v6h-4v-4h-4V6zM6 34v6c0 1.1.9 2 2 2h6v-4H10v-4H6zm32 8h6c1.1 0 2-.9 2-2v-6h-4v4h-4v4zM6 22h36v4H6z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 277 B |
4
miniprogram/images/icon-store.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||||
|
<path fill="#666666" d="M24 4L6 14v4h4v22h28V18h4v-4L24 4zm8 30H16V22h16v12z"/>
|
||||||
|
<rect x="20" y="26" width="8" height="8" fill="#999"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 235 B |
7
miniprogram/images/product-default.svg
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" width="200" height="200">
|
||||||
|
<rect width="200" height="200" fill="#f5f5f5"/>
|
||||||
|
<rect x="50" y="50" width="100" height="100" fill="#e8e8e8" rx="8"/>
|
||||||
|
<rect x="70" y="70" width="60" height="40" fill="#d9d9d9" rx="4"/>
|
||||||
|
<rect x="70" y="120" width="40" height="10" fill="#bfbfbf" rx="2"/>
|
||||||
|
<rect x="70" y="135" width="25" height="8" fill="#faad14" rx="2"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 435 B |
@ -3,10 +3,10 @@
|
|||||||
<!-- 门店选择 -->
|
<!-- 门店选择 -->
|
||||||
<view class="store-selector" bindtap="selectStore">
|
<view class="store-selector" bindtap="selectStore">
|
||||||
<view class="store-info">
|
<view class="store-info">
|
||||||
<image class="store-icon" src="/images/icon-store.png" mode="aspectFit"></image>
|
<image class="store-icon" src="/images/icon-store.svg" mode="aspectFit"></image>
|
||||||
<text class="store-name">{{currentStore.storeName || '选择门店'}}</text>
|
<text class="store-name">{{currentStore.storeName || '选择门店'}}</text>
|
||||||
</view>
|
</view>
|
||||||
<image class="arrow-icon" src="/images/icon-arrow.png" mode="aspectFit"></image>
|
<image class="arrow-icon" src="/images/icon-arrow.svg" mode="aspectFit"></image>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 性别筛选 -->
|
<!-- 性别筛选 -->
|
||||||
@ -51,7 +51,7 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="col-user">
|
<view class="col-user">
|
||||||
<image class="avatar" src="{{item.avatar || '/images/avatar-default.png'}}" mode="aspectFill"></image>
|
<image class="avatar" src="{{item.avatar || '/images/avatar-default.svg'}}" mode="aspectFill"></image>
|
||||||
<view class="user-info">
|
<view class="user-info">
|
||||||
<text class="name">{{item.realName}}</text>
|
<text class="name">{{item.realName}}</text>
|
||||||
<text class="rate">胜率 {{item.winRate}}%</text>
|
<text class="rate">胜率 {{item.winRate}}%</text>
|
||||||
@ -67,7 +67,7 @@
|
|||||||
</block>
|
</block>
|
||||||
|
|
||||||
<view wx:else class="empty-state">
|
<view wx:else class="empty-state">
|
||||||
<image src="/images/empty-ranking.png" mode="aspectFit"></image>
|
<image src="/images/empty-ranking.svg" mode="aspectFit"></image>
|
||||||
<text>暂无排名数据</text>
|
<text>暂无排名数据</text>
|
||||||
<text class="sub-text">每月完成3场比赛即可上榜</text>
|
<text class="sub-text">每月完成3场比赛即可上榜</text>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
<view class="container">
|
<view class="container">
|
||||||
<!-- 未登录或非天梯用户提示 -->
|
<!-- 未登录或非天梯用户提示 -->
|
||||||
<view class="notice-card" wx:if="{{!ladderUser}}">
|
<view class="notice-card" wx:if="{{!ladderUser}}">
|
||||||
<image src="/images/icon-info.png" mode="aspectFit"></image>
|
<image src="/images/icon-info.svg" mode="aspectFit"></image>
|
||||||
<text>仅天梯用户可使用比赛功能,请联系门店工作人员加入天梯系统</text>
|
<text>仅天梯用户可使用比赛功能,请联系门店工作人员加入天梯系统</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@ -11,7 +11,7 @@
|
|||||||
<!-- 我的信息 -->
|
<!-- 我的信息 -->
|
||||||
<view class="my-info-card">
|
<view class="my-info-card">
|
||||||
<view class="info-header">
|
<view class="info-header">
|
||||||
<image class="avatar" src="{{userInfo.avatar || '/images/avatar-default.png'}}" mode="aspectFill"></image>
|
<image class="avatar" src="{{userInfo.avatar || '/images/avatar-default.svg'}}" mode="aspectFill"></image>
|
||||||
<view class="info-meta">
|
<view class="info-meta">
|
||||||
<text class="name">{{ladderUser.realName}}</text>
|
<text class="name">{{ladderUser.realName}}</text>
|
||||||
<view class="level-power">
|
<view class="level-power">
|
||||||
@ -25,12 +25,12 @@
|
|||||||
<!-- 挑战赛入口 -->
|
<!-- 挑战赛入口 -->
|
||||||
<view class="action-card">
|
<view class="action-card">
|
||||||
<view class="card-title">
|
<view class="card-title">
|
||||||
<image src="/images/icon-challenge.png" mode="aspectFit"></image>
|
<image src="/images/icon-challenge.svg" mode="aspectFit"></image>
|
||||||
<text>挑战赛</text>
|
<text>挑战赛</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="card-desc">扫描对手会员码发起挑战,挑战赛权重 x1.5</view>
|
<view class="card-desc">扫描对手会员码发起挑战,挑战赛权重 x1.5</view>
|
||||||
<button class="btn-primary" bindtap="startChallenge">
|
<button class="btn-primary" bindtap="startChallenge">
|
||||||
<image src="/images/icon-scan.png" mode="aspectFit"></image>
|
<image src="/images/icon-scan.svg" mode="aspectFit"></image>
|
||||||
扫码挑战
|
扫码挑战
|
||||||
</button>
|
</button>
|
||||||
</view>
|
</view>
|
||||||
@ -38,12 +38,12 @@
|
|||||||
<!-- 排位赛入口 -->
|
<!-- 排位赛入口 -->
|
||||||
<view class="action-card">
|
<view class="action-card">
|
||||||
<view class="card-title">
|
<view class="card-title">
|
||||||
<image src="/images/icon-ranking.png" mode="aspectFit"></image>
|
<image src="/images/icon-ranking.svg" mode="aspectFit"></image>
|
||||||
<text>排位赛</text>
|
<text>排位赛</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="card-desc">扫描比赛二维码加入排位赛</view>
|
<view class="card-desc">扫描比赛二维码加入排位赛</view>
|
||||||
<button class="btn-secondary" bindtap="joinRankingMatch">
|
<button class="btn-secondary" bindtap="joinRankingMatch">
|
||||||
<image src="/images/icon-scan.png" mode="aspectFit"></image>
|
<image src="/images/icon-scan.svg" mode="aspectFit"></image>
|
||||||
扫码加入
|
扫码加入
|
||||||
</button>
|
</button>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@ -25,7 +25,7 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="empty-state" wx:else>
|
<view class="empty-state" wx:else>
|
||||||
<image src="/images/empty-match.png" mode="aspectFit"></image>
|
<image src="/images/empty-match.svg" mode="aspectFit"></image>
|
||||||
<text>暂无比赛记录</text>
|
<text>暂无比赛记录</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
|||||||
@ -8,11 +8,11 @@
|
|||||||
</view>
|
</view>
|
||||||
<view class="points-actions">
|
<view class="points-actions">
|
||||||
<view class="action-btn" bindtap="goToRecords">
|
<view class="action-btn" bindtap="goToRecords">
|
||||||
<image src="/images/icon-records.png" mode="aspectFit"></image>
|
<image src="/images/icon-records.svg" mode="aspectFit"></image>
|
||||||
<text>积分记录</text>
|
<text>积分记录</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="action-btn" bindtap="goToOrders">
|
<view class="action-btn" bindtap="goToOrders">
|
||||||
<image src="/images/icon-order.png" mode="aspectFit"></image>
|
<image src="/images/icon-order.svg" mode="aspectFit"></image>
|
||||||
<text>我的订单</text>
|
<text>我的订单</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@ -30,7 +30,7 @@
|
|||||||
bindtap="viewProduct"
|
bindtap="viewProduct"
|
||||||
data-product="{{item}}"
|
data-product="{{item}}"
|
||||||
>
|
>
|
||||||
<image class="product-image" src="{{item.image || '/images/product-default.png'}}" mode="aspectFill"></image>
|
<image class="product-image" src="{{item.image || '/images/product-default.svg'}}" mode="aspectFill"></image>
|
||||||
<view class="product-info">
|
<view class="product-info">
|
||||||
<text class="product-name">{{item.name}}</text>
|
<text class="product-name">{{item.name}}</text>
|
||||||
<view class="product-meta">
|
<view class="product-meta">
|
||||||
@ -46,7 +46,7 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="empty-state" wx:else>
|
<view class="empty-state" wx:else>
|
||||||
<image src="/images/empty-products.png" mode="aspectFit"></image>
|
<image src="/images/empty-products.svg" mode="aspectFit"></image>
|
||||||
<text>暂无可兑换商品</text>
|
<text>暂无可兑换商品</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@ -60,7 +60,7 @@
|
|||||||
<!-- 商品详情弹窗 -->
|
<!-- 商品详情弹窗 -->
|
||||||
<view class="product-modal" wx:if="{{showProductModal}}" bindtap="closeProductModal">
|
<view class="product-modal" wx:if="{{showProductModal}}" bindtap="closeProductModal">
|
||||||
<view class="modal-content" catchtap="">
|
<view class="modal-content" catchtap="">
|
||||||
<image class="modal-image" src="{{currentProduct.image || '/images/product-default.png'}}" mode="aspectFill"></image>
|
<image class="modal-image" src="{{currentProduct.image || '/images/product-default.svg'}}" mode="aspectFill"></image>
|
||||||
<view class="modal-info">
|
<view class="modal-info">
|
||||||
<text class="modal-name">{{currentProduct.name}}</text>
|
<text class="modal-name">{{currentProduct.name}}</text>
|
||||||
<text class="modal-desc">{{currentProduct.description || '暂无描述'}}</text>
|
<text class="modal-desc">{{currentProduct.description || '暂无描述'}}</text>
|
||||||
|
|||||||
@ -33,7 +33,7 @@
|
|||||||
<text class="order-status status-{{item.status}}">{{getStatusText(item.status)}}</text>
|
<text class="order-status status-{{item.status}}">{{getStatusText(item.status)}}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="order-content">
|
<view class="order-content">
|
||||||
<image class="product-image" src="{{item.productImage || '/images/product-default.png'}}" mode="aspectFill"></image>
|
<image class="product-image" src="{{item.productImage || '/images/product-default.svg'}}" mode="aspectFill"></image>
|
||||||
<view class="product-info">
|
<view class="product-info">
|
||||||
<text class="product-name">{{item.productName}}</text>
|
<text class="product-name">{{item.productName}}</text>
|
||||||
<text class="store-name">{{item.storeName}}</text>
|
<text class="store-name">{{item.storeName}}</text>
|
||||||
@ -48,7 +48,7 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="empty-state" wx:else>
|
<view class="empty-state" wx:else>
|
||||||
<image src="/images/empty-order.png" mode="aspectFit"></image>
|
<image src="/images/empty-order.svg" mode="aspectFit"></image>
|
||||||
<text>暂无订单</text>
|
<text>暂无订单</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@ -67,7 +67,7 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="modal-body">
|
<view class="modal-body">
|
||||||
<image class="product-image" src="{{currentOrder.productImage || '/images/product-default.png'}}" mode="aspectFill"></image>
|
<image class="product-image" src="{{currentOrder.productImage || '/images/product-default.svg'}}" mode="aspectFill"></image>
|
||||||
<text class="product-name">{{currentOrder.productName}}</text>
|
<text class="product-name">{{currentOrder.productName}}</text>
|
||||||
<text class="points-used">使用积分: {{currentOrder.pointsUsed}}</text>
|
<text class="points-used">使用积分: {{currentOrder.pointsUsed}}</text>
|
||||||
|
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="empty-state" wx:else>
|
<view class="empty-state" wx:else>
|
||||||
<image src="/images/empty-records.png" mode="aspectFit"></image>
|
<image src="/images/empty-records.svg" mode="aspectFit"></image>
|
||||||
<text>暂无积分记录</text>
|
<text>暂无积分记录</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
|||||||
@ -17,13 +17,13 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="store-check" wx:if="{{currentStoreId === item.id}}">
|
<view class="store-check" wx:if="{{currentStoreId === item.id}}">
|
||||||
<image src="/images/icon-check.png" mode="aspectFit"></image>
|
<image src="/images/icon-check.svg" mode="aspectFit"></image>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="empty-state" wx:if="{{stores.length === 0}}">
|
<view class="empty-state" wx:if="{{stores.length === 0}}">
|
||||||
<image src="/images/empty-store.png" mode="aspectFit"></image>
|
<image src="/images/empty-store.svg" mode="aspectFit"></image>
|
||||||
<text>暂无门店</text>
|
<text>暂无门店</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@ -6,7 +6,9 @@ Page({
|
|||||||
userInfo: null,
|
userInfo: null,
|
||||||
ladderUser: null,
|
ladderUser: null,
|
||||||
currentStore: null,
|
currentStore: null,
|
||||||
showQrcode: false
|
showQrcode: false,
|
||||||
|
needProfile: false,
|
||||||
|
tempUserProfile: null
|
||||||
},
|
},
|
||||||
|
|
||||||
onLoad() {
|
onLoad() {
|
||||||
@ -18,11 +20,18 @@ Page({
|
|||||||
},
|
},
|
||||||
|
|
||||||
async initData() {
|
async initData() {
|
||||||
if (!app.globalData.token) {
|
// 先进行微信登录获取openid
|
||||||
return
|
if (!app.globalData.wxLoginInfo) {
|
||||||
|
try {
|
||||||
|
await app.wxLogin()
|
||||||
|
} catch (e) {
|
||||||
|
console.error('微信登录失败:', e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (app.globalData.token) {
|
||||||
await this.refreshData()
|
await this.refreshData()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async refreshData() {
|
async refreshData() {
|
||||||
@ -40,20 +49,80 @@ Page({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async handleLogin() {
|
// 获取手机号授权
|
||||||
|
async onGetPhoneNumber(e) {
|
||||||
|
if (e.detail.errMsg !== 'getPhoneNumber:ok') {
|
||||||
|
wx.showToast({ title: '需要授权手机号才能登录', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
wx.showLoading({ title: '登录中...' })
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await app.login()
|
// 如果没有微信登录信息,先登录
|
||||||
|
if (!app.globalData.wxLoginInfo) {
|
||||||
|
await app.wxLogin()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户头像昵称
|
||||||
|
let userProfile = this.data.tempUserProfile
|
||||||
|
if (!userProfile) {
|
||||||
|
try {
|
||||||
|
const profileRes = await wx.getUserProfile({
|
||||||
|
desc: '用于完善会员资料'
|
||||||
|
})
|
||||||
|
userProfile = profileRes.userInfo
|
||||||
|
} catch (err) {
|
||||||
|
// 用户拒绝授权头像昵称,使用默认值
|
||||||
|
userProfile = { nickName: '新用户', avatarUrl: '' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手机号登录
|
||||||
|
await app.phoneLogin(e.detail.encryptedData, e.detail.iv, userProfile)
|
||||||
|
|
||||||
|
// 获取门店信息
|
||||||
await app.getCurrentStore()
|
await app.getCurrentStore()
|
||||||
|
|
||||||
this.setData({
|
this.setData({
|
||||||
userInfo: app.globalData.userInfo,
|
userInfo: app.globalData.userInfo,
|
||||||
currentStore: app.globalData.currentStore
|
ladderUser: app.globalData.ladderUser,
|
||||||
|
currentStore: app.globalData.currentStore,
|
||||||
|
needProfile: false,
|
||||||
|
tempUserProfile: null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
wx.hideLoading()
|
||||||
wx.showToast({ title: '登录成功', icon: 'success' })
|
wx.showToast({ title: '登录成功', icon: 'success' })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
wx.showToast({ title: '登录失败', icon: 'none' })
|
wx.hideLoading()
|
||||||
|
console.error('登录失败:', e)
|
||||||
|
wx.showToast({ title: e.message || '登录失败', icon: 'none' })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 选择头像
|
||||||
|
async onChooseAvatar() {
|
||||||
|
try {
|
||||||
|
const res = await wx.getUserProfile({
|
||||||
|
desc: '用于完善会员资料'
|
||||||
|
})
|
||||||
|
this.setData({
|
||||||
|
tempUserProfile: res.userInfo,
|
||||||
|
needProfile: false
|
||||||
|
})
|
||||||
|
wx.showToast({ title: '已获取头像昵称', icon: 'success' })
|
||||||
|
} catch (e) {
|
||||||
|
wx.showToast({ title: '获取头像昵称失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 旧的登录方法(兼容)
|
||||||
|
async handleLogin() {
|
||||||
|
// 触发手机号授权按钮
|
||||||
|
wx.showToast({ title: '请点击手机号登录按钮', icon: 'none' })
|
||||||
|
},
|
||||||
|
|
||||||
showMemberCode() {
|
showMemberCode() {
|
||||||
if (!this.data.userInfo?.memberCode) return
|
if (!this.data.userInfo?.memberCode) return
|
||||||
|
|
||||||
|
|||||||
@ -2,21 +2,39 @@
|
|||||||
<view class="container">
|
<view class="container">
|
||||||
<!-- 用户信息卡片 -->
|
<!-- 用户信息卡片 -->
|
||||||
<view class="user-card">
|
<view class="user-card">
|
||||||
<view class="user-header" wx:if="{{userInfo}}">
|
<!-- 已登录状态 -->
|
||||||
<image class="avatar-large" src="{{userInfo.avatar || '/images/avatar-default.png'}}" mode="aspectFill"></image>
|
<view class="user-header" wx:if="{{userInfo && userInfo.phone}}">
|
||||||
|
<image class="avatar-large" src="{{userInfo.avatar || '/images/avatar-default.svg'}}" mode="aspectFill"></image>
|
||||||
<view class="user-meta">
|
<view class="user-meta">
|
||||||
<text class="nickname">{{userInfo.nickname || '新用户'}}</text>
|
<text class="nickname">{{userInfo.nickname || '新用户'}}</text>
|
||||||
<view class="member-code" bindtap="showMemberCode">
|
<view class="member-code" bindtap="showMemberCode">
|
||||||
<text>会员码: {{userInfo.memberCode}}</text>
|
<text>会员码: {{userInfo.memberCode}}</text>
|
||||||
<image src="/images/icon-qrcode.png" mode="aspectFit"></image>
|
<image src="/images/icon-qrcode.svg" mode="aspectFit"></image>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="user-header" wx:else bindtap="handleLogin">
|
|
||||||
<image class="avatar-large" src="/images/avatar-default.png" mode="aspectFill"></image>
|
<!-- 未登录状态 -->
|
||||||
<view class="user-meta">
|
<view class="login-panel" wx:else>
|
||||||
<text class="nickname">点击登录</text>
|
<image class="avatar-large" src="/images/avatar-default.svg" mode="aspectFill"></image>
|
||||||
|
<view class="login-tips">
|
||||||
|
<text class="title">欢迎来到羽动俱乐部</text>
|
||||||
|
<text class="desc">授权手机号,开启您的运动之旅</text>
|
||||||
</view>
|
</view>
|
||||||
|
<button
|
||||||
|
class="login-btn phone-btn"
|
||||||
|
open-type="getPhoneNumber"
|
||||||
|
bindgetphonenumber="onGetPhoneNumber"
|
||||||
|
>
|
||||||
|
手机号快捷登录
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="login-btn profile-btn"
|
||||||
|
bindtap="onChooseAvatar"
|
||||||
|
wx:if="{{needProfile}}"
|
||||||
|
>
|
||||||
|
完善头像昵称
|
||||||
|
</button>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 积分展示 -->
|
<!-- 积分展示 -->
|
||||||
@ -50,31 +68,31 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="notice-card" wx:else>
|
<view class="notice-card" wx:else>
|
||||||
<image src="/images/icon-info.png" mode="aspectFit"></image>
|
<image src="/images/icon-info.svg" mode="aspectFit"></image>
|
||||||
<text>您还不是天梯用户,请联系门店工作人员加入天梯系统</text>
|
<text>您还不是天梯用户,请联系门店工作人员加入天梯系统</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 功能菜单 -->
|
<!-- 功能菜单 -->
|
||||||
<view class="menu-list">
|
<view class="menu-list">
|
||||||
<view class="menu-item" bindtap="goTo" data-url="/pages/match/history/index">
|
<view class="menu-item" bindtap="goTo" data-url="/pages/match/history/index">
|
||||||
<image src="/images/icon-history.png" mode="aspectFit"></image>
|
<image src="/images/icon-history.svg" mode="aspectFit"></image>
|
||||||
<text>比赛记录</text>
|
<text>比赛记录</text>
|
||||||
<image class="arrow" src="/images/icon-arrow.png" mode="aspectFit"></image>
|
<image class="arrow" src="/images/icon-arrow.svg" mode="aspectFit"></image>
|
||||||
</view>
|
</view>
|
||||||
<view class="menu-item" bindtap="goTo" data-url="/pages/points/records/index">
|
<view class="menu-item" bindtap="goTo" data-url="/pages/points/records/index">
|
||||||
<image src="/images/icon-points.png" mode="aspectFit"></image>
|
<image src="/images/icon-points.svg" mode="aspectFit"></image>
|
||||||
<text>积分记录</text>
|
<text>积分记录</text>
|
||||||
<image class="arrow" src="/images/icon-arrow.png" mode="aspectFit"></image>
|
<image class="arrow" src="/images/icon-arrow.svg" mode="aspectFit"></image>
|
||||||
</view>
|
</view>
|
||||||
<view class="menu-item" bindtap="goTo" data-url="/pages/points/order/index">
|
<view class="menu-item" bindtap="goTo" data-url="/pages/points/order/index">
|
||||||
<image src="/images/icon-order.png" mode="aspectFit"></image>
|
<image src="/images/icon-order.svg" mode="aspectFit"></image>
|
||||||
<text>兑换订单</text>
|
<text>兑换订单</text>
|
||||||
<image class="arrow" src="/images/icon-arrow.png" mode="aspectFit"></image>
|
<image class="arrow" src="/images/icon-arrow.svg" mode="aspectFit"></image>
|
||||||
</view>
|
</view>
|
||||||
<view class="menu-item" bindtap="goTo" data-url="/pages/store/index">
|
<view class="menu-item" bindtap="goTo" data-url="/pages/store/index">
|
||||||
<image src="/images/icon-store.png" mode="aspectFit"></image>
|
<image src="/images/icon-store.svg" mode="aspectFit"></image>
|
||||||
<text>切换门店</text>
|
<text>切换门店</text>
|
||||||
<image class="arrow" src="/images/icon-arrow.png" mode="aspectFit"></image>
|
<image class="arrow" src="/images/icon-arrow.svg" mode="aspectFit"></image>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@ -21,6 +21,61 @@
|
|||||||
margin-right: 24rpx;
|
margin-right: 24rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 登录面板 */
|
||||||
|
.login-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 40rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-panel .avatar-large {
|
||||||
|
width: 160rpx;
|
||||||
|
height: 160rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 4rpx solid rgba(255, 255, 255, 0.3);
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-tips {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-tips .title {
|
||||||
|
display: block;
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-tips .desc {
|
||||||
|
display: block;
|
||||||
|
font-size: 26rpx;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn {
|
||||||
|
width: 80%;
|
||||||
|
height: 88rpx;
|
||||||
|
line-height: 88rpx;
|
||||||
|
border-radius: 44rpx;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn.phone-btn {
|
||||||
|
background: #fff;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn.profile-btn {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
.user-meta {
|
.user-meta {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|||||||
201
miniprogram/scripts/generateImages.js
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
/**
|
||||||
|
* 生成小程序所需的图标和图片
|
||||||
|
* 运行: node scripts/generateImages.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
const imagesDir = path.join(__dirname, '..', 'images')
|
||||||
|
|
||||||
|
// 确保 images 目录存在
|
||||||
|
if (!fs.existsSync(imagesDir)) {
|
||||||
|
fs.mkdirSync(imagesDir, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
// SVG 图标定义 (48x48)
|
||||||
|
const icons = {
|
||||||
|
// 箭头图标
|
||||||
|
'icon-arrow': `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||||
|
<path fill="#999999" d="M18 12l2.83-2.83L32.66 21 20.83 32.83 18 30l9-9z"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
// 勾选图标
|
||||||
|
'icon-check': `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||||
|
<circle cx="24" cy="24" r="20" fill="#52c41a"/>
|
||||||
|
<path fill="#ffffff" d="M20 30.59l-6.29-6.3 2.12-2.12L20 26.34l12.17-12.17 2.12 2.12z"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
// 挑战图标
|
||||||
|
'icon-challenge': `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||||
|
<path fill="#ff6b35" d="M24 4l5.5 11.5L42 17l-9 9 2 12.5L24 33l-11 5.5 2-12.5-9-9 12.5-1.5z"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
// 历史图标
|
||||||
|
'icon-history': `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||||
|
<path fill="#666666" d="M25.99 6C16.04 6 8 14.06 8 24H2l7.79 7.79.14.29L18 24h-6c0-7.73 6.27-14 14-14s14 6.27 14 14-6.27 14-14 14c-3.87 0-7.36-1.58-9.89-4.11l-2.83 2.83C16.53 39.98 21.02 42 26 42c9.94 0 18-8.06 18-18S35.94 6 25.99 6zM24 16v10l8.56 5.08 1.44-2.43-7-4.15V16h-3z"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
// 信息图标
|
||||||
|
'icon-info': `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||||
|
<circle cx="24" cy="24" r="20" fill="#1890ff"/>
|
||||||
|
<text x="24" y="32" text-anchor="middle" fill="#ffffff" font-size="24" font-weight="bold">i</text>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
// 订单图标
|
||||||
|
'icon-order': `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||||
|
<path fill="#666666" d="M38 6H10c-2.21 0-4 1.79-4 4v28c0 2.21 1.79 4 4 4h28c2.21 0 4-1.79 4-4V10c0-2.21-1.79-4-4-4zm-22 6h16v4H16v-4zm16 20H16v-4h16v4zm0-8H16v-4h16v4z"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
// 积分图标
|
||||||
|
'icon-points': `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||||
|
<circle cx="24" cy="24" r="18" fill="#faad14"/>
|
||||||
|
<text x="24" y="30" text-anchor="middle" fill="#ffffff" font-size="18" font-weight="bold">P</text>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
// 二维码图标
|
||||||
|
'icon-qrcode': `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||||
|
<path fill="#666666" d="M8 8h14v14H8V8zm4 4v6h6v-6h-6zm12-4h14v14H24V8zm4 4v6h6v-6h-6zM8 26h14v14H8V26zm4 4v6h6v-6h-6zm16-4h4v4h-4zm4 4h4v4h-4zm-4 4h4v4h-4zm4 4h4v4h-4zm4-8h4v4h-4zm0 8h4v4h-4z"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
// 排名图标
|
||||||
|
'icon-ranking': `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||||
|
<rect x="6" y="26" width="10" height="16" fill="#52c41a"/>
|
||||||
|
<rect x="19" y="14" width="10" height="28" fill="#faad14"/>
|
||||||
|
<rect x="32" y="20" width="10" height="22" fill="#1890ff"/>
|
||||||
|
<text x="11" y="24" text-anchor="middle" fill="#666" font-size="10">2</text>
|
||||||
|
<text x="24" y="12" text-anchor="middle" fill="#666" font-size="10">1</text>
|
||||||
|
<text x="37" y="18" text-anchor="middle" fill="#666" font-size="10">3</text>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
// 记录图标
|
||||||
|
'icon-records': `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||||
|
<path fill="#666666" d="M14 6c-2.21 0-4 1.79-4 4v28c0 2.21 1.79 4 4 4h20c2.21 0 4-1.79 4-4V10c0-2.21-1.79-4-4-4H14zm2 8h16v4H16v-4zm0 8h12v4H16v-4zm0 8h16v4H16v-4z"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
// 扫码图标
|
||||||
|
'icon-scan': `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||||
|
<path fill="#666666" d="M6 14V8c0-1.1.9-2 2-2h6v4H10v4H6zm32-8h6c1.1 0 2 .9 2 2v6h-4v-4h-4V6zM6 34v6c0 1.1.9 2 2 2h6v-4H10v-4H6zm32 8h6c1.1 0 2-.9 2-2v-6h-4v4h-4v4zM6 22h36v4H6z"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
// 门店图标
|
||||||
|
'icon-store': `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||||
|
<path fill="#666666" d="M24 4L6 14v4h4v22h28V18h4v-4L24 4zm8 30H16V22h16v12z"/>
|
||||||
|
<rect x="20" y="26" width="8" height="8" fill="#999"/>
|
||||||
|
</svg>`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 空状态图片 (200x160)
|
||||||
|
const emptyImages = {
|
||||||
|
// 空排名
|
||||||
|
'empty-ranking': `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 160" width="200" height="160">
|
||||||
|
<rect x="40" y="80" width="30" height="50" fill="#e8e8e8" rx="4"/>
|
||||||
|
<rect x="85" y="50" width="30" height="80" fill="#e8e8e8" rx="4"/>
|
||||||
|
<rect x="130" y="95" width="30" height="35" fill="#e8e8e8" rx="4"/>
|
||||||
|
<circle cx="55" cy="65" r="12" fill="#d9d9d9"/>
|
||||||
|
<circle cx="100" cy="35" r="12" fill="#d9d9d9"/>
|
||||||
|
<circle cx="145" cy="80" r="12" fill="#d9d9d9"/>
|
||||||
|
<text x="100" y="150" text-anchor="middle" fill="#bfbfbf" font-size="14">暂无排名数据</text>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
// 空门店
|
||||||
|
'empty-store': `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 160" width="200" height="160">
|
||||||
|
<rect x="50" y="40" width="100" height="70" fill="#e8e8e8" rx="8"/>
|
||||||
|
<rect x="60" y="50" width="25" height="20" fill="#d9d9d9" rx="2"/>
|
||||||
|
<rect x="90" y="50" width="50" height="10" fill="#d9d9d9" rx="2"/>
|
||||||
|
<rect x="90" y="65" width="40" height="8" fill="#f5f5f5" rx="2"/>
|
||||||
|
<path d="M70 85 L80 95 L130 95 L130 110 L80 110 Z" fill="#d9d9d9"/>
|
||||||
|
<text x="100" y="140" text-anchor="middle" fill="#bfbfbf" font-size="14">暂无门店</text>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
// 空比赛
|
||||||
|
'empty-match': `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 160" width="200" height="160">
|
||||||
|
<circle cx="70" cy="60" r="25" fill="#e8e8e8"/>
|
||||||
|
<circle cx="130" cy="60" r="25" fill="#e8e8e8"/>
|
||||||
|
<text x="100" y="68" text-anchor="middle" fill="#d9d9d9" font-size="24">VS</text>
|
||||||
|
<rect x="50" y="100" width="100" height="20" fill="#e8e8e8" rx="4"/>
|
||||||
|
<text x="100" y="150" text-anchor="middle" fill="#bfbfbf" font-size="14">暂无比赛记录</text>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
// 空记录
|
||||||
|
'empty-records': `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 160" width="200" height="160">
|
||||||
|
<rect x="50" y="30" width="100" height="90" fill="#e8e8e8" rx="8"/>
|
||||||
|
<rect x="65" y="45" width="70" height="8" fill="#d9d9d9" rx="2"/>
|
||||||
|
<rect x="65" y="60" width="50" height="8" fill="#f5f5f5" rx="2"/>
|
||||||
|
<rect x="65" y="75" width="60" height="8" fill="#d9d9d9" rx="2"/>
|
||||||
|
<rect x="65" y="90" width="40" height="8" fill="#f5f5f5" rx="2"/>
|
||||||
|
<rect x="65" y="105" width="55" height="8" fill="#d9d9d9" rx="2"/>
|
||||||
|
<text x="100" y="145" text-anchor="middle" fill="#bfbfbf" font-size="14">暂无记录</text>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
// 空商品
|
||||||
|
'empty-products': `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 160" width="200" height="160">
|
||||||
|
<rect x="40" y="30" width="50" height="70" fill="#e8e8e8" rx="6"/>
|
||||||
|
<rect x="110" y="30" width="50" height="70" fill="#e8e8e8" rx="6"/>
|
||||||
|
<rect x="50" y="40" width="30" height="30" fill="#d9d9d9" rx="4"/>
|
||||||
|
<rect x="120" y="40" width="30" height="30" fill="#d9d9d9" rx="4"/>
|
||||||
|
<rect x="50" y="75" width="30" height="6" fill="#f5f5f5" rx="2"/>
|
||||||
|
<rect x="120" y="75" width="30" height="6" fill="#f5f5f5" rx="2"/>
|
||||||
|
<rect x="50" y="85" width="20" height="8" fill="#faad14" rx="2"/>
|
||||||
|
<rect x="120" y="85" width="20" height="8" fill="#faad14" rx="2"/>
|
||||||
|
<text x="100" y="130" text-anchor="middle" fill="#bfbfbf" font-size="14">暂无商品</text>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
// 空订单
|
||||||
|
'empty-order': `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 160" width="200" height="160">
|
||||||
|
<rect x="50" y="25" width="100" height="100" fill="#e8e8e8" rx="8"/>
|
||||||
|
<rect x="65" y="40" width="70" height="35" fill="#d9d9d9" rx="4"/>
|
||||||
|
<rect x="65" y="85" width="45" height="10" fill="#f5f5f5" rx="2"/>
|
||||||
|
<rect x="65" y="100" width="30" height="15" fill="#faad14" rx="3"/>
|
||||||
|
<circle cx="130" cy="107" r="8" fill="#d9d9d9"/>
|
||||||
|
<text x="100" y="145" text-anchor="middle" fill="#bfbfbf" font-size="14">暂无订单</text>
|
||||||
|
</svg>`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认图片
|
||||||
|
const defaultImages = {
|
||||||
|
// 默认头像 (120x120)
|
||||||
|
'avatar-default': `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
|
||||||
|
<rect width="120" height="120" fill="#e8e8e8"/>
|
||||||
|
<circle cx="60" cy="45" r="25" fill="#bfbfbf"/>
|
||||||
|
<ellipse cx="60" cy="100" rx="40" ry="30" fill="#bfbfbf"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
// 默认商品图 (200x200)
|
||||||
|
'product-default': `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" width="200" height="200">
|
||||||
|
<rect width="200" height="200" fill="#f5f5f5"/>
|
||||||
|
<rect x="50" y="50" width="100" height="100" fill="#e8e8e8" rx="8"/>
|
||||||
|
<rect x="70" y="70" width="60" height="40" fill="#d9d9d9" rx="4"/>
|
||||||
|
<rect x="70" y="120" width="40" height="10" fill="#bfbfbf" rx="2"/>
|
||||||
|
<rect x="70" y="135" width="25" height="8" fill="#faad14" rx="2"/>
|
||||||
|
</svg>`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入 SVG 文件
|
||||||
|
function writeSvgFile(name, content) {
|
||||||
|
const filePath = path.join(imagesDir, `${name}.svg`)
|
||||||
|
fs.writeFileSync(filePath, content.trim())
|
||||||
|
console.log(`✓ 已生成: ${name}.svg`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成所有图标
|
||||||
|
console.log('\n=== 生成图标 ===')
|
||||||
|
Object.entries(icons).forEach(([name, svg]) => {
|
||||||
|
writeSvgFile(name, svg)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 生成所有空状态图片
|
||||||
|
console.log('\n=== 生成空状态图片 ===')
|
||||||
|
Object.entries(emptyImages).forEach(([name, svg]) => {
|
||||||
|
writeSvgFile(name, svg)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 生成默认图片
|
||||||
|
console.log('\n=== 生成默认图片 ===')
|
||||||
|
Object.entries(defaultImages).forEach(([name, svg]) => {
|
||||||
|
writeSvgFile(name, svg)
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('\n所有图片生成完成!')
|
||||||
|
console.log('提示: 微信小程序支持 SVG 格式图片,可直接使用')
|
||||||
|
console.log('如需 PNG 格式,可使用在线工具转换或安装 sharp 库')
|
||||||
@ -97,16 +97,12 @@ class LadderAdminController {
|
|||||||
return res.status(400).json(error('该手机号在此门店已存在天梯用户', 400));
|
return res.status(400).json(error('该手机号在此门店已存在天梯用户', 400));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查找或创建基础用户
|
// 查找已注册的微信用户(可能不存在)
|
||||||
let user = await User.findOne({ where: { phone } });
|
const user = await User.findOne({ where: { phone } });
|
||||||
if (!user) {
|
|
||||||
// 手机号对应的用户不存在,无法创建天梯用户
|
|
||||||
return res.status(400).json(error('该手机号用户未注册小程序,请先引导用户注册', 400));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建天梯用户
|
// 创建天梯用户(允许user_id为空,待微信用户登录后自动关联)
|
||||||
const ladderUser = await LadderUser.create({
|
const ladderUser = await LadderUser.create({
|
||||||
user_id: user.id,
|
user_id: user ? user.id : null, // 如果微信用户存在则关联,否则为空
|
||||||
store_id: targetStoreId,
|
store_id: targetStoreId,
|
||||||
real_name,
|
real_name,
|
||||||
phone,
|
phone,
|
||||||
@ -116,7 +112,11 @@ class LadderAdminController {
|
|||||||
status: 1
|
status: 1
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json(success({ id: ladderUser.id }, '创建成功'));
|
const message = user
|
||||||
|
? '创建成功,已关联微信用户'
|
||||||
|
: '创建成功,待用户注册小程序后自动关联';
|
||||||
|
|
||||||
|
res.json(success({ id: ladderUser.id, linked: !!user }, message));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('创建天梯用户失败:', err);
|
console.error('创建天梯用户失败:', err);
|
||||||
res.status(500).json(error('创建失败'));
|
res.status(500).json(error('创建失败'));
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
|
const crypto = require('crypto');
|
||||||
const { User, LadderUser, Store, Match, MatchGame } = require('../models');
|
const { User, LadderUser, Store, Match, MatchGame } = require('../models');
|
||||||
const { generateMemberCode, success, error, calculateDistance } = require('../utils/helper');
|
const { generateMemberCode, success, error, calculateDistance } = require('../utils/helper');
|
||||||
const { Op } = require('sequelize');
|
const { Op } = require('sequelize');
|
||||||
|
|
||||||
class UserController {
|
class UserController {
|
||||||
// 微信登录
|
// 微信登录(获取 session_key,用于后续手机号解密)
|
||||||
async login(req, res) {
|
async login(req, res) {
|
||||||
try {
|
try {
|
||||||
const { code } = req.body;
|
const { code } = req.body;
|
||||||
@ -14,7 +15,7 @@ class UserController {
|
|||||||
return res.status(400).json(error('缺少登录code', 400));
|
return res.status(400).json(error('缺少登录code', 400));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取微信openid
|
// 获取微信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: {
|
params: {
|
||||||
appid: process.env.WX_APPID,
|
appid: process.env.WX_APPID,
|
||||||
@ -28,7 +29,92 @@ class UserController {
|
|||||||
return res.status(400).json(error('微信登录失败: ' + wxRes.data.errmsg, 400));
|
return res.status(400).json(error('微信登录失败: ' + wxRes.data.errmsg, 400));
|
||||||
}
|
}
|
||||||
|
|
||||||
const { openid, unionid } = wxRes.data;
|
const { openid, unionid, session_key } = wxRes.data;
|
||||||
|
|
||||||
|
// 查找用户
|
||||||
|
let user = await User.findOne({ where: { openid } });
|
||||||
|
let isNewUser = false;
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
isNewUser = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回登录信息(包含session_key用于后续手机号解密)
|
||||||
|
// 注意:实际生产环境中session_key不应该直接返回给前端
|
||||||
|
// 这里为了简化流程,使用加密后的session_key
|
||||||
|
const encryptedSessionKey = this.encryptSessionKey(session_key, openid);
|
||||||
|
|
||||||
|
res.json(success({
|
||||||
|
openid,
|
||||||
|
unionid,
|
||||||
|
sessionKey: encryptedSessionKey,
|
||||||
|
isNewUser,
|
||||||
|
hasPhone: user?.phone ? true : false,
|
||||||
|
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 ? '请授权手机号完成注册' : '登录成功'));
|
||||||
|
} catch (err) {
|
||||||
|
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 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');
|
||||||
|
return encrypted;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解密session_key
|
||||||
|
decryptSessionKey(encryptedSessionKey, openid) {
|
||||||
|
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');
|
||||||
|
return decrypted;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手机号授权登录(解密手机号并完成注册/登录)
|
||||||
|
async phoneLogin(req, res) {
|
||||||
|
try {
|
||||||
|
const { openid, unionid, sessionKey, encryptedData, iv, nickname, avatar, gender } = req.body;
|
||||||
|
|
||||||
|
if (!openid || !sessionKey || !encryptedData || !iv) {
|
||||||
|
return res.status(400).json(error('参数不完整', 400));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解密session_key
|
||||||
|
let realSessionKey;
|
||||||
|
try {
|
||||||
|
realSessionKey = this.decryptSessionKey(sessionKey, openid);
|
||||||
|
} catch (e) {
|
||||||
|
return res.status(400).json(error('会话已过期,请重新登录', 400));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解密手机号
|
||||||
|
let phone;
|
||||||
|
try {
|
||||||
|
phone = this.decryptPhoneNumber(realSessionKey, encryptedData, iv);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('手机号解密失败:', e);
|
||||||
|
return res.status(400).json(error('手机号解密失败,请重新授权', 400));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!phone) {
|
||||||
|
return res.status(400).json(error('获取手机号失败', 400));
|
||||||
|
}
|
||||||
|
|
||||||
// 查找或创建用户
|
// 查找或创建用户
|
||||||
let user = await User.findOne({ where: { openid } });
|
let user = await User.findOne({ where: { openid } });
|
||||||
@ -38,10 +124,27 @@ class UserController {
|
|||||||
user = await User.create({
|
user = await User.create({
|
||||||
openid,
|
openid,
|
||||||
unionid,
|
unionid,
|
||||||
|
phone,
|
||||||
member_code: generateMemberCode(),
|
member_code: generateMemberCode(),
|
||||||
nickname: '新用户',
|
nickname: nickname || '新用户',
|
||||||
|
avatar: avatar || '',
|
||||||
|
gender: gender || 0,
|
||||||
status: 1
|
status: 1
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 关联已存在的天梯用户(通过手机号)
|
||||||
|
await this.linkLadderUsers(user.id, phone);
|
||||||
|
} else {
|
||||||
|
// 更新用户信息
|
||||||
|
const updateData = { phone };
|
||||||
|
if (nickname) updateData.nickname = nickname;
|
||||||
|
if (avatar) updateData.avatar = avatar;
|
||||||
|
if (gender !== undefined) updateData.gender = gender;
|
||||||
|
|
||||||
|
await user.update(updateData);
|
||||||
|
|
||||||
|
// 如果手机号变化,重新关联天梯用户
|
||||||
|
await this.linkLadderUsers(user.id, phone);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成token
|
// 生成token
|
||||||
@ -51,6 +154,12 @@ class UserController {
|
|||||||
{ expiresIn: process.env.JWT_EXPIRES_IN || '7d' }
|
{ 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'] }]
|
||||||
|
});
|
||||||
|
|
||||||
res.json(success({
|
res.json(success({
|
||||||
token,
|
token,
|
||||||
userInfo: {
|
userInfo: {
|
||||||
@ -60,15 +169,84 @@ class UserController {
|
|||||||
phone: user.phone,
|
phone: user.phone,
|
||||||
gender: user.gender,
|
gender: user.gender,
|
||||||
memberCode: user.member_code,
|
memberCode: user.member_code,
|
||||||
totalPoints: user.total_points
|
totalPoints: user.total_points,
|
||||||
|
ladderUsers: ladderUsers.map(lu => ({
|
||||||
|
id: lu.id,
|
||||||
|
storeId: lu.store_id,
|
||||||
|
storeName: lu.store?.name,
|
||||||
|
realName: lu.real_name,
|
||||||
|
level: lu.level,
|
||||||
|
powerScore: lu.power_score
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
}, '登录成功'));
|
}, '登录成功'));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('登录失败:', err);
|
console.error('手机号登录失败:', err);
|
||||||
res.status(500).json(error('登录失败'));
|
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 decipher = crypto.createDecipheriv('aes-128-cbc', sessionKeyBuffer, ivBuffer);
|
||||||
|
decipher.setAutoPadding(true);
|
||||||
|
|
||||||
|
let decoded = decipher.update(encryptedDataBuffer, 'binary', 'utf8');
|
||||||
|
decoded += decipher.final('utf8');
|
||||||
|
|
||||||
|
const result = JSON.parse(decoded);
|
||||||
|
return result.phoneNumber || result.purePhoneNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关联天梯用户(通过手机号)
|
||||||
|
async linkLadderUsers(userId, phone) {
|
||||||
|
// 查找所有未关联的天梯用户(相同手机号)
|
||||||
|
const unlinkedLadderUsers = await LadderUser.findAll({
|
||||||
|
where: {
|
||||||
|
phone,
|
||||||
|
user_id: null,
|
||||||
|
status: 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 批量更新关联
|
||||||
|
if (unlinkedLadderUsers.length > 0) {
|
||||||
|
await LadderUser.update(
|
||||||
|
{ user_id: userId },
|
||||||
|
{ where: { phone, user_id: null } }
|
||||||
|
);
|
||||||
|
console.log(`已关联 ${unlinkedLadderUsers.length} 个天梯用户到用户 ${userId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新用户资料(头像、昵称)
|
||||||
|
async updateProfile(req, res) {
|
||||||
|
try {
|
||||||
|
const { nickname, avatar, gender } = req.body;
|
||||||
|
const user = req.user;
|
||||||
|
|
||||||
|
const updateData = {};
|
||||||
|
if (nickname) updateData.nickname = nickname;
|
||||||
|
if (avatar) updateData.avatar = avatar;
|
||||||
|
if (gender !== undefined) updateData.gender = gender;
|
||||||
|
|
||||||
|
await user.update(updateData);
|
||||||
|
|
||||||
|
res.json(success({
|
||||||
|
nickname: user.nickname,
|
||||||
|
avatar: user.avatar,
|
||||||
|
gender: user.gender
|
||||||
|
}, '更新成功'));
|
||||||
|
} catch (err) {
|
||||||
|
console.error('更新资料失败:', err);
|
||||||
|
res.status(500).json(error('更新失败'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 获取用户信息
|
// 获取用户信息
|
||||||
async getInfo(req, res) {
|
async getInfo(req, res) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -9,8 +9,8 @@ const LadderUser = sequelize.define('LadderUser', {
|
|||||||
},
|
},
|
||||||
user_id: {
|
user_id: {
|
||||||
type: DataTypes.BIGINT,
|
type: DataTypes.BIGINT,
|
||||||
allowNull: false,
|
allowNull: true,
|
||||||
comment: '关联用户ID'
|
comment: '关联用户ID(可为空,待微信用户登录后通过手机号关联)'
|
||||||
},
|
},
|
||||||
store_id: {
|
store_id: {
|
||||||
type: DataTypes.BIGINT,
|
type: DataTypes.BIGINT,
|
||||||
|
|||||||
@ -3,9 +3,15 @@ const router = express.Router();
|
|||||||
const userController = require('../controllers/userController');
|
const userController = require('../controllers/userController');
|
||||||
const { authUser } = require('../middlewares/auth');
|
const { authUser } = require('../middlewares/auth');
|
||||||
|
|
||||||
// 微信登录
|
// 微信登录(获取openid和session_key)
|
||||||
router.post('/login', userController.login);
|
router.post('/login', userController.login);
|
||||||
|
|
||||||
|
// 手机号授权登录(完成注册/登录)
|
||||||
|
router.post('/phone-login', userController.phoneLogin);
|
||||||
|
|
||||||
|
// 更新用户资料(头像、昵称)
|
||||||
|
router.put('/profile', authUser, userController.updateProfile);
|
||||||
|
|
||||||
// 获取用户信息
|
// 获取用户信息
|
||||||
router.get('/info', authUser, userController.getInfo);
|
router.get('/info', authUser, userController.getInfo);
|
||||||
|
|
||||||
|
|||||||
60
server/src/scripts/fixLadderUserForeignKey.js
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* 修复 ladder_users 表的 user_id 外键约束
|
||||||
|
* 运行: node src/scripts/fixLadderUserForeignKey.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
require('dotenv').config();
|
||||||
|
const sequelize = require('../config/database');
|
||||||
|
|
||||||
|
async function fixForeignKey() {
|
||||||
|
try {
|
||||||
|
console.log('开始修复 ladder_users 表的 user_id 外键...\n');
|
||||||
|
|
||||||
|
// 1. 删除已存在的外键约束
|
||||||
|
console.log('1. 删除已存在的外键约束...');
|
||||||
|
const [constraints] = await sequelize.query(`
|
||||||
|
SELECT CONSTRAINT_NAME
|
||||||
|
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
|
||||||
|
WHERE TABLE_NAME = 'ladder_users'
|
||||||
|
AND COLUMN_NAME = 'user_id'
|
||||||
|
AND REFERENCED_TABLE_NAME IS NOT NULL
|
||||||
|
AND TABLE_SCHEMA = '${process.env.DB_NAME || 'yingsha'}'
|
||||||
|
`);
|
||||||
|
|
||||||
|
for (const constraint of constraints) {
|
||||||
|
console.log(` 删除外键: ${constraint.CONSTRAINT_NAME}`);
|
||||||
|
try {
|
||||||
|
await sequelize.query(`ALTER TABLE ladder_users DROP FOREIGN KEY ${constraint.CONSTRAINT_NAME}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(` 跳过: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 修改 user_id 列为可空
|
||||||
|
console.log('\n2. 修改 user_id 列为可空...');
|
||||||
|
await sequelize.query(`
|
||||||
|
ALTER TABLE ladder_users
|
||||||
|
MODIFY COLUMN user_id BIGINT NULL
|
||||||
|
COMMENT '关联用户ID(可为空,待微信用户登录后通过手机号关联)'
|
||||||
|
`);
|
||||||
|
console.log(' 完成');
|
||||||
|
|
||||||
|
// 3. 重新添加外键约束(允许 SET NULL)
|
||||||
|
console.log('\n3. 重新添加外键约束(ON DELETE SET NULL)...');
|
||||||
|
await sequelize.query(`
|
||||||
|
ALTER TABLE ladder_users
|
||||||
|
ADD CONSTRAINT fk_ladder_users_user
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||||
|
ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
`);
|
||||||
|
console.log(' 完成');
|
||||||
|
|
||||||
|
console.log('\n✅ 修复完成!现在可以重新启动服务器了。');
|
||||||
|
process.exit(0);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('\n❌ 修复失败:', err.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fixForeignKey();
|
||||||