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",
|
||||
"configHash": "0bd4dba1",
|
||||
"lockfileHash": "39601b45",
|
||||
"browserHash": "a4d88a8a",
|
||||
"browserHash": "b4faa0d4",
|
||||
"optimized": {
|
||||
"@element-plus/icons-vue": {
|
||||
"src": "../../@element-plus/icons-vue/dist/index.js",
|
||||
"file": "@element-plus_icons-vue.js",
|
||||
"fileHash": "7f008ba6",
|
||||
"fileHash": "a0465eac",
|
||||
"needsInterop": false
|
||||
},
|
||||
"axios": {
|
||||
"src": "../../axios/index.js",
|
||||
"file": "axios.js",
|
||||
"fileHash": "3f18dca8",
|
||||
"fileHash": "5dd68ace",
|
||||
"needsInterop": false
|
||||
},
|
||||
"dayjs": {
|
||||
"src": "../../dayjs/dayjs.min.js",
|
||||
"file": "dayjs.js",
|
||||
"fileHash": "58ba4ccf",
|
||||
"fileHash": "43487a3d",
|
||||
"needsInterop": true
|
||||
},
|
||||
"element-plus": {
|
||||
"src": "../../element-plus/es/index.mjs",
|
||||
"file": "element-plus.js",
|
||||
"fileHash": "3f46101b",
|
||||
"fileHash": "641ecf31",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/dist/locale/zh-cn.mjs": {
|
||||
"src": "../../element-plus/dist/locale/zh-cn.mjs",
|
||||
"file": "element-plus_dist_locale_zh-cn__mjs.js",
|
||||
"fileHash": "7c78b45c",
|
||||
"fileHash": "c69676ce",
|
||||
"needsInterop": false
|
||||
},
|
||||
"pinia": {
|
||||
"src": "../../pinia/dist/pinia.mjs",
|
||||
"file": "pinia.js",
|
||||
"fileHash": "af5f9503",
|
||||
"fileHash": "bfa2bd84",
|
||||
"needsInterop": false
|
||||
},
|
||||
"vue": {
|
||||
"src": "../../vue/dist/vue.runtime.esm-bundler.js",
|
||||
"file": "vue.js",
|
||||
"fileHash": "8671a44a",
|
||||
"fileHash": "95941b62",
|
||||
"needsInterop": false
|
||||
},
|
||||
"vue-router": {
|
||||
"src": "../../vue-router/dist/vue-router.mjs",
|
||||
"file": "vue-router.js",
|
||||
"fileHash": "e5335c2b",
|
||||
"fileHash": "799d6557",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es": {
|
||||
"src": "../../element-plus/es/index.mjs",
|
||||
"file": "element-plus_es.js",
|
||||
"fileHash": "294409a4",
|
||||
"fileHash": "bf7435a6",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/base/style/css": {
|
||||
"src": "../../element-plus/es/components/base/style/css.mjs",
|
||||
"file": "element-plus_es_components_base_style_css.js",
|
||||
"fileHash": "6f0ffffd",
|
||||
"fileHash": "69992cb3",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/form/style/css": {
|
||||
"src": "../../element-plus/es/components/form/style/css.mjs",
|
||||
"file": "element-plus_es_components_form_style_css.js",
|
||||
"fileHash": "db280542",
|
||||
"fileHash": "ebec77cf",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/button/style/css": {
|
||||
"src": "../../element-plus/es/components/button/style/css.mjs",
|
||||
"file": "element-plus_es_components_button_style_css.js",
|
||||
"fileHash": "647ba0c4",
|
||||
"fileHash": "911a2184",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/form-item/style/css": {
|
||||
"src": "../../element-plus/es/components/form-item/style/css.mjs",
|
||||
"file": "element-plus_es_components_form-item_style_css.js",
|
||||
"fileHash": "df94624f",
|
||||
"fileHash": "36203619",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/input/style/css": {
|
||||
"src": "../../element-plus/es/components/input/style/css.mjs",
|
||||
"file": "element-plus_es_components_input_style_css.js",
|
||||
"fileHash": "6ce4b79b",
|
||||
"fileHash": "3cb1f96d",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/dialog/style/css": {
|
||||
"src": "../../element-plus/es/components/dialog/style/css.mjs",
|
||||
"file": "element-plus_es_components_dialog_style_css.js",
|
||||
"fileHash": "09e38fa4",
|
||||
"fileHash": "76e79a0a",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/container/style/css": {
|
||||
"src": "../../element-plus/es/components/container/style/css.mjs",
|
||||
"file": "element-plus_es_components_container_style_css.js",
|
||||
"fileHash": "9f75a15b",
|
||||
"fileHash": "3871ef81",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/main/style/css": {
|
||||
"src": "../../element-plus/es/components/main/style/css.mjs",
|
||||
"file": "element-plus_es_components_main_style_css.js",
|
||||
"fileHash": "b7d52e31",
|
||||
"fileHash": "3d6a71ca",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/header/style/css": {
|
||||
"src": "../../element-plus/es/components/header/style/css.mjs",
|
||||
"file": "element-plus_es_components_header_style_css.js",
|
||||
"fileHash": "dc7cbd51",
|
||||
"fileHash": "88603798",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/dropdown/style/css": {
|
||||
"src": "../../element-plus/es/components/dropdown/style/css.mjs",
|
||||
"file": "element-plus_es_components_dropdown_style_css.js",
|
||||
"fileHash": "c25ab9b5",
|
||||
"fileHash": "9e766544",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/dropdown-menu/style/css": {
|
||||
"src": "../../element-plus/es/components/dropdown-menu/style/css.mjs",
|
||||
"file": "element-plus_es_components_dropdown-menu_style_css.js",
|
||||
"fileHash": "36878229",
|
||||
"fileHash": "b8031298",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/dropdown-item/style/css": {
|
||||
"src": "../../element-plus/es/components/dropdown-item/style/css.mjs",
|
||||
"file": "element-plus_es_components_dropdown-item_style_css.js",
|
||||
"fileHash": "3f098ecd",
|
||||
"fileHash": "a1a95087",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/avatar/style/css": {
|
||||
"src": "../../element-plus/es/components/avatar/style/css.mjs",
|
||||
"file": "element-plus_es_components_avatar_style_css.js",
|
||||
"fileHash": "a302ad09",
|
||||
"fileHash": "9aca49c4",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/breadcrumb/style/css": {
|
||||
"src": "../../element-plus/es/components/breadcrumb/style/css.mjs",
|
||||
"file": "element-plus_es_components_breadcrumb_style_css.js",
|
||||
"fileHash": "630e60e3",
|
||||
"fileHash": "3bc001aa",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/breadcrumb-item/style/css": {
|
||||
"src": "../../element-plus/es/components/breadcrumb-item/style/css.mjs",
|
||||
"file": "element-plus_es_components_breadcrumb-item_style_css.js",
|
||||
"fileHash": "3d33f916",
|
||||
"fileHash": "be124fe0",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/aside/style/css": {
|
||||
"src": "../../element-plus/es/components/aside/style/css.mjs",
|
||||
"file": "element-plus_es_components_aside_style_css.js",
|
||||
"fileHash": "52b6f7bc",
|
||||
"fileHash": "c068ded5",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/menu/style/css": {
|
||||
"src": "../../element-plus/es/components/menu/style/css.mjs",
|
||||
"file": "element-plus_es_components_menu_style_css.js",
|
||||
"fileHash": "77e7329d",
|
||||
"fileHash": "403f5873",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/menu-item/style/css": {
|
||||
"src": "../../element-plus/es/components/menu-item/style/css.mjs",
|
||||
"file": "element-plus_es_components_menu-item_style_css.js",
|
||||
"fileHash": "ae2a2097",
|
||||
"fileHash": "27234326",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/icon/style/css": {
|
||||
"src": "../../element-plus/es/components/icon/style/css.mjs",
|
||||
"file": "element-plus_es_components_icon_style_css.js",
|
||||
"fileHash": "c16e8e96",
|
||||
"fileHash": "235345a9",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/input-number/style/css": {
|
||||
"src": "../../element-plus/es/components/input-number/style/css.mjs",
|
||||
"file": "element-plus_es_components_input-number_style_css.js",
|
||||
"fileHash": "0ef981fb",
|
||||
"fileHash": "9b08cd36",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/tag/style/css": {
|
||||
"src": "../../element-plus/es/components/tag/style/css.mjs",
|
||||
"file": "element-plus_es_components_tag_style_css.js",
|
||||
"fileHash": "7268bcfa",
|
||||
"fileHash": "9f589faf",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/row/style/css": {
|
||||
"src": "../../element-plus/es/components/row/style/css.mjs",
|
||||
"file": "element-plus_es_components_row_style_css.js",
|
||||
"fileHash": "3ebd4979",
|
||||
"fileHash": "638d7e8c",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/col/style/css": {
|
||||
"src": "../../element-plus/es/components/col/style/css.mjs",
|
||||
"file": "element-plus_es_components_col_style_css.js",
|
||||
"fileHash": "2d165654",
|
||||
"fileHash": "bb1da678",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/loading/style/css": {
|
||||
"src": "../../element-plus/es/components/loading/style/css.mjs",
|
||||
"file": "element-plus_es_components_loading_style_css.js",
|
||||
"fileHash": "88f7c147",
|
||||
"fileHash": "eef180be",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/descriptions/style/css": {
|
||||
"src": "../../element-plus/es/components/descriptions/style/css.mjs",
|
||||
"file": "element-plus_es_components_descriptions_style_css.js",
|
||||
"fileHash": "ce70f737",
|
||||
"fileHash": "4cb57571",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/descriptions-item/style/css": {
|
||||
"src": "../../element-plus/es/components/descriptions-item/style/css.mjs",
|
||||
"file": "element-plus_es_components_descriptions-item_style_css.js",
|
||||
"fileHash": "8dd7f2f1",
|
||||
"fileHash": "0f719728",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/pagination/style/css": {
|
||||
"src": "../../element-plus/es/components/pagination/style/css.mjs",
|
||||
"file": "element-plus_es_components_pagination_style_css.js",
|
||||
"fileHash": "62918e26",
|
||||
"fileHash": "42e6236a",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/table/style/css": {
|
||||
"src": "../../element-plus/es/components/table/style/css.mjs",
|
||||
"file": "element-plus_es_components_table_style_css.js",
|
||||
"fileHash": "d66cccc2",
|
||||
"fileHash": "20763031",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/table-column/style/css": {
|
||||
"src": "../../element-plus/es/components/table-column/style/css.mjs",
|
||||
"file": "element-plus_es_components_table-column_style_css.js",
|
||||
"fileHash": "4b9e1b7d",
|
||||
"fileHash": "2b02ba24",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/select/style/css": {
|
||||
"src": "../../element-plus/es/components/select/style/css.mjs",
|
||||
"file": "element-plus_es_components_select_style_css.js",
|
||||
"fileHash": "2b082cb1",
|
||||
"fileHash": "06a565f3",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/option/style/css": {
|
||||
"src": "../../element-plus/es/components/option/style/css.mjs",
|
||||
"file": "element-plus_es_components_option_style_css.js",
|
||||
"fileHash": "dcd074a6",
|
||||
"fileHash": "0c0e3f42",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/switch/style/css": {
|
||||
"src": "../../element-plus/es/components/switch/style/css.mjs",
|
||||
"file": "element-plus_es_components_switch_style_css.js",
|
||||
"fileHash": "eb467983",
|
||||
"fileHash": "b8c8ea9a",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/radio-group/style/css": {
|
||||
"src": "../../element-plus/es/components/radio-group/style/css.mjs",
|
||||
"file": "element-plus_es_components_radio-group_style_css.js",
|
||||
"fileHash": "5557d38c",
|
||||
"fileHash": "4174369f",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/radio/style/css": {
|
||||
"src": "../../element-plus/es/components/radio/style/css.mjs",
|
||||
"file": "element-plus_es_components_radio_style_css.js",
|
||||
"fileHash": "98bc52ba",
|
||||
"fileHash": "3b71271e",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/upload/style/css": {
|
||||
"src": "../../element-plus/es/components/upload/style/css.mjs",
|
||||
"file": "element-plus_es_components_upload_style_css.js",
|
||||
"fileHash": "6b2b7aff",
|
||||
"fileHash": "e7cad837",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/image/style/css": {
|
||||
"src": "../../element-plus/es/components/image/style/css.mjs",
|
||||
"file": "element-plus_es_components_image_style_css.js",
|
||||
"fileHash": "7e74c215",
|
||||
"fileHash": "4e0a87a4",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/divider/style/css": {
|
||||
"src": "../../element-plus/es/components/divider/style/css.mjs",
|
||||
"file": "element-plus_es_components_divider_style_css.js",
|
||||
"fileHash": "6356af7f",
|
||||
"fileHash": "c846d7f7",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/text/style/css": {
|
||||
"src": "../../element-plus/es/components/text/style/css.mjs",
|
||||
"file": "element-plus_es_components_text_style_css.js",
|
||||
"fileHash": "591aafc3",
|
||||
"fileHash": "3e8a90d0",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/collapse/style/css": {
|
||||
"src": "../../element-plus/es/components/collapse/style/css.mjs",
|
||||
"file": "element-plus_es_components_collapse_style_css.js",
|
||||
"fileHash": "7310bb21",
|
||||
"fileHash": "51560080",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/es/components/collapse-item/style/css": {
|
||||
"src": "../../element-plus/es/components/collapse-item/style/css.mjs",
|
||||
"file": "element-plus_es_components_collapse-item_style_css.js",
|
||||
"fileHash": "27e07e55",
|
||||
"fileHash": "94bca0a2",
|
||||
"needsInterop": false
|
||||
},
|
||||
"qrcode": {
|
||||
"src": "../../qrcode/lib/browser.js",
|
||||
"file": "qrcode.js",
|
||||
"fileHash": "ad0f2956",
|
||||
"needsInterop": true
|
||||
}
|
||||
},
|
||||
"chunks": {
|
||||
@ -321,6 +327,9 @@
|
||||
"chunk-5KK3TTMN": {
|
||||
"file": "chunk-5KK3TTMN.js"
|
||||
},
|
||||
"chunk-NKQWFVTF": {
|
||||
"file": "chunk-NKQWFVTF.js"
|
||||
},
|
||||
"chunk-REWOA3VH": {
|
||||
"file": "chunk-REWOA3VH.js"
|
||||
},
|
||||
@ -330,9 +339,6 @@
|
||||
"chunk-SMFPDFTD": {
|
||||
"file": "chunk-SMFPDFTD.js"
|
||||
},
|
||||
"chunk-NKQWFVTF": {
|
||||
"file": "chunk-NKQWFVTF.js"
|
||||
},
|
||||
"chunk-IV6PSERC": {
|
||||
"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-UBLR4G7Q.js";
|
||||
import "./chunk-5KK3TTMN.js";
|
||||
import "./chunk-NKQWFVTF.js";
|
||||
import "./chunk-REWOA3VH.js";
|
||||
import "./chunk-TX5YLZ4O.js";
|
||||
import "./chunk-NKQWFVTF.js";
|
||||
import "./chunk-IV6PSERC.js";
|
||||
|
||||
// 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",
|
||||
"element-plus": "^2.4.4",
|
||||
"pinia": "^2.1.7",
|
||||
"qrcode": "^1.5.3",
|
||||
"qrcode": "^1.5.4",
|
||||
"vue": "^3.4.0",
|
||||
"vue-router": "^4.2.5"
|
||||
},
|
||||
|
||||
@ -9,21 +9,21 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"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",
|
||||
"axios": "^1.6.2",
|
||||
"dayjs": "^1.11.10",
|
||||
"echarts": "^5.4.3",
|
||||
"qrcode": "^1.5.3",
|
||||
"dayjs": "^1.11.10"
|
||||
"element-plus": "^2.4.4",
|
||||
"pinia": "^2.1.7",
|
||||
"qrcode": "^1.5.4",
|
||||
"vue": "^3.4.0",
|
||||
"vue-router": "^4.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.5.2",
|
||||
"vite": "^5.0.10",
|
||||
"sass": "^1.69.5",
|
||||
"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-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 v-if="userStore.isSuperAdmin" prop="storeName" label="门店" width="140" />
|
||||
<el-table-column prop="type" label="类型" width="100">
|
||||
@ -110,14 +117,38 @@
|
||||
<el-button type="primary" @click="handleCreate">创建</el-button>
|
||||
</template>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, onMounted, nextTick } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { View, Download, Printer } from '@element-plus/icons-vue'
|
||||
import dayjs from 'dayjs'
|
||||
import QRCode from 'qrcode'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { getMatches, createMatch, startMatch, endMatch, getStores } from '@/api/admin'
|
||||
|
||||
@ -130,6 +161,12 @@ const stores = ref([])
|
||||
const searchForm = ref({ store_id: '', type: '', status: '' })
|
||||
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 createFormRef = 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(() => {
|
||||
fetchStores()
|
||||
fetchData()
|
||||
})
|
||||
</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({
|
||||
globalData: {
|
||||
userInfo: null,
|
||||
token: null,
|
||||
currentStore: null,
|
||||
ladderUser: null,
|
||||
wsConnected: false
|
||||
wsConnected: false,
|
||||
// 微信登录临时信息
|
||||
wxLoginInfo: null,
|
||||
// 从配置文件读取
|
||||
baseUrl: config.baseUrl,
|
||||
wsUrl: config.wsUrl,
|
||||
},
|
||||
|
||||
onLaunch() {
|
||||
// 从本地存储读取token
|
||||
const token = wx.getStorageSync('token')
|
||||
const token = wx.getStorageSync("token");
|
||||
if (token) {
|
||||
this.globalData.token = token
|
||||
this.getUserInfo()
|
||||
this.globalData.token = token;
|
||||
this.getUserInfo();
|
||||
}
|
||||
},
|
||||
|
||||
// 登录
|
||||
login() {
|
||||
// 微信登录(第一步:获取openid和session_key)
|
||||
wxLogin() {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.login({
|
||||
success: res => {
|
||||
success: (res) => {
|
||||
wx.request({
|
||||
url: `${this.globalData.baseUrl}/api/user/login`,
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
data: { code: res.code },
|
||||
success: loginRes => {
|
||||
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)
|
||||
const data = loginRes.data.data;
|
||||
// 保存微信登录信息(用于后续手机号授权)
|
||||
this.globalData.wxLoginInfo = {
|
||||
openid: data.openid,
|
||||
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 {
|
||||
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() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.request('/api/user/info').then(res => {
|
||||
this.globalData.userInfo = res.data
|
||||
this.connectWebSocket()
|
||||
resolve(res.data)
|
||||
}).catch(reject)
|
||||
this.request("/api/user/info")
|
||||
.then((res) => {
|
||||
this.globalData.userInfo = res.data;
|
||||
this.connectWebSocket();
|
||||
resolve(res.data);
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
},
|
||||
|
||||
// 获取当前门店
|
||||
getCurrentStore() {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.getLocation({
|
||||
type: 'gcj02',
|
||||
success: loc => {
|
||||
this.request('/api/user/current-store', {
|
||||
type: "gcj02",
|
||||
success: (loc) => {
|
||||
this.request("/api/user/current-store", {
|
||||
latitude: loc.latitude,
|
||||
longitude: loc.longitude
|
||||
}).then(res => {
|
||||
this.globalData.currentStore = res.data
|
||||
longitude: loc.longitude,
|
||||
})
|
||||
.then((res) => {
|
||||
this.globalData.currentStore = res.data;
|
||||
if (res.data?.ladderUserId) {
|
||||
this.getLadderUser(res.data.storeId)
|
||||
this.getLadderUser(res.data.storeId);
|
||||
}
|
||||
resolve(res.data)
|
||||
}).catch(reject)
|
||||
resolve(res.data);
|
||||
})
|
||||
.catch(reject);
|
||||
},
|
||||
fail: () => {
|
||||
// 无法获取位置,使用默认门店
|
||||
this.request('/api/user/current-store').then(res => {
|
||||
this.globalData.currentStore = res.data
|
||||
resolve(res.data)
|
||||
}).catch(reject)
|
||||
}
|
||||
})
|
||||
this.request("/api/user/current-store")
|
||||
.then((res) => {
|
||||
this.globalData.currentStore = res.data;
|
||||
resolve(res.data);
|
||||
})
|
||||
.catch(reject);
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// 获取天梯用户信息
|
||||
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) {
|
||||
this.globalData.ladderUser = res.data[0]
|
||||
this.globalData.ladderUser = res.data[0];
|
||||
}
|
||||
return res.data
|
||||
})
|
||||
return res.data;
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
// WebSocket连接
|
||||
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({
|
||||
url: wsUrl,
|
||||
success: () => {
|
||||
console.log('WebSocket连接中...')
|
||||
}
|
||||
})
|
||||
console.log("WebSocket连接中...");
|
||||
},
|
||||
});
|
||||
|
||||
wx.onSocketOpen(() => {
|
||||
console.log('WebSocket已连接')
|
||||
this.globalData.wsConnected = true
|
||||
console.log("WebSocket已连接");
|
||||
this.globalData.wsConnected = true;
|
||||
// 发送认证
|
||||
wx.sendSocketMessage({
|
||||
data: JSON.stringify({
|
||||
type: 'auth',
|
||||
token: this.globalData.token
|
||||
})
|
||||
})
|
||||
})
|
||||
type: "auth",
|
||||
token: this.globalData.token,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
wx.onSocketMessage(res => {
|
||||
const data = JSON.parse(res.data)
|
||||
this.handleWsMessage(data)
|
||||
})
|
||||
wx.onSocketMessage((res) => {
|
||||
const data = JSON.parse(res.data);
|
||||
this.handleWsMessage(data);
|
||||
});
|
||||
|
||||
wx.onSocketClose(() => {
|
||||
console.log('WebSocket已断开')
|
||||
this.globalData.wsConnected = false
|
||||
console.log("WebSocket已断开");
|
||||
this.globalData.wsConnected = false;
|
||||
// 尝试重连
|
||||
setTimeout(() => {
|
||||
this.connectWebSocket()
|
||||
}, 5000)
|
||||
})
|
||||
this.connectWebSocket();
|
||||
}, 5000);
|
||||
});
|
||||
|
||||
wx.onSocketError(err => {
|
||||
console.error('WebSocket错误:', err)
|
||||
})
|
||||
wx.onSocketError((err) => {
|
||||
console.error("WebSocket错误:", err);
|
||||
});
|
||||
},
|
||||
|
||||
// 处理WebSocket消息
|
||||
handleWsMessage(data) {
|
||||
switch (data.type) {
|
||||
case 'challenge_request':
|
||||
case "challenge_request":
|
||||
// 收到挑战请求
|
||||
wx.showModal({
|
||||
title: '收到挑战',
|
||||
title: "收到挑战",
|
||||
content: `${data.data.challenger.realName} 向你发起挑战`,
|
||||
confirmText: '接受',
|
||||
cancelText: '拒绝',
|
||||
success: res => {
|
||||
this.request('/api/match/challenge/respond', {
|
||||
confirmText: "接受",
|
||||
cancelText: "拒绝",
|
||||
success: (res) => {
|
||||
this.request(
|
||||
"/api/match/challenge/respond",
|
||||
{
|
||||
match_id: data.data.matchId,
|
||||
accept: res.confirm
|
||||
}, 'POST')
|
||||
}
|
||||
})
|
||||
break
|
||||
case 'score_confirm_request':
|
||||
accept: res.confirm,
|
||||
},
|
||||
"POST"
|
||||
);
|
||||
},
|
||||
});
|
||||
break;
|
||||
case "score_confirm_request":
|
||||
// 收到比分确认请求
|
||||
wx.showModal({
|
||||
title: '确认比分',
|
||||
title: "确认比分",
|
||||
content: `比分: ${data.data.player1Score} : ${data.data.player2Score}`,
|
||||
confirmText: '确认',
|
||||
cancelText: '有争议',
|
||||
success: res => {
|
||||
this.request('/api/match/challenge/confirm-score', {
|
||||
confirmText: "确认",
|
||||
cancelText: "有争议",
|
||||
success: (res) => {
|
||||
this.request(
|
||||
"/api/match/challenge/confirm-score",
|
||||
{
|
||||
game_id: data.data.gameId,
|
||||
confirm: res.confirm
|
||||
}, 'POST')
|
||||
}
|
||||
})
|
||||
break
|
||||
case 'match_paired':
|
||||
confirm: res.confirm,
|
||||
},
|
||||
"POST"
|
||||
);
|
||||
},
|
||||
});
|
||||
break;
|
||||
case "match_paired":
|
||||
// 排位赛匹配通知
|
||||
wx.showModal({
|
||||
title: '匹配成功',
|
||||
title: "匹配成功",
|
||||
content: `你的对手是: ${data.data.opponent.realName}`,
|
||||
showCancel: false
|
||||
})
|
||||
break
|
||||
showCancel: false,
|
||||
});
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
// 封装请求
|
||||
request(url, data = {}, method = 'GET') {
|
||||
request(url, data = {}, method = "GET") {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: `${this.globalData.baseUrl}${url}`,
|
||||
method,
|
||||
data,
|
||||
header: {
|
||||
'Authorization': `Bearer ${this.globalData.token}`
|
||||
Authorization: `Bearer ${this.globalData.token}`,
|
||||
},
|
||||
success: res => {
|
||||
success: (res) => {
|
||||
if (res.data.code === 0) {
|
||||
resolve(res.data)
|
||||
resolve(res.data);
|
||||
} else if (res.data.code === 401) {
|
||||
// 登录过期
|
||||
this.globalData.token = null
|
||||
wx.removeStorageSync('token')
|
||||
wx.reLaunch({ url: '/pages/user/index' })
|
||||
reject(res.data)
|
||||
this.globalData.token = null;
|
||||
wx.removeStorageSync("token");
|
||||
wx.reLaunch({ url: "/pages/user/index" });
|
||||
reject(res.data);
|
||||
} else {
|
||||
wx.showToast({ title: res.data.message, icon: 'none' })
|
||||
reject(res.data)
|
||||
wx.showToast({ title: res.data.message, icon: "none" });
|
||||
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-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>
|
||||
</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>
|
||||
|
||||
<!-- 性别筛选 -->
|
||||
@ -51,7 +51,7 @@
|
||||
</view>
|
||||
</view>
|
||||
<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">
|
||||
<text class="name">{{item.realName}}</text>
|
||||
<text class="rate">胜率 {{item.winRate}}%</text>
|
||||
@ -67,7 +67,7 @@
|
||||
</block>
|
||||
|
||||
<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 class="sub-text">每月完成3场比赛即可上榜</text>
|
||||
</view>
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
<view class="container">
|
||||
<!-- 未登录或非天梯用户提示 -->
|
||||
<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>
|
||||
</view>
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
<!-- 我的信息 -->
|
||||
<view class="my-info-card">
|
||||
<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">
|
||||
<text class="name">{{ladderUser.realName}}</text>
|
||||
<view class="level-power">
|
||||
@ -25,12 +25,12 @@
|
||||
<!-- 挑战赛入口 -->
|
||||
<view class="action-card">
|
||||
<view class="card-title">
|
||||
<image src="/images/icon-challenge.png" mode="aspectFit"></image>
|
||||
<image src="/images/icon-challenge.svg" mode="aspectFit"></image>
|
||||
<text>挑战赛</text>
|
||||
</view>
|
||||
<view class="card-desc">扫描对手会员码发起挑战,挑战赛权重 x1.5</view>
|
||||
<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>
|
||||
</view>
|
||||
@ -38,12 +38,12 @@
|
||||
<!-- 排位赛入口 -->
|
||||
<view class="action-card">
|
||||
<view class="card-title">
|
||||
<image src="/images/icon-ranking.png" mode="aspectFit"></image>
|
||||
<image src="/images/icon-ranking.svg" mode="aspectFit"></image>
|
||||
<text>排位赛</text>
|
||||
</view>
|
||||
<view class="card-desc">扫描比赛二维码加入排位赛</view>
|
||||
<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>
|
||||
</view>
|
||||
|
||||
@ -25,7 +25,7 @@
|
||||
</view>
|
||||
|
||||
<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>
|
||||
</view>
|
||||
|
||||
|
||||
@ -8,11 +8,11 @@
|
||||
</view>
|
||||
<view class="points-actions">
|
||||
<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>
|
||||
</view>
|
||||
<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>
|
||||
</view>
|
||||
</view>
|
||||
@ -30,7 +30,7 @@
|
||||
bindtap="viewProduct"
|
||||
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">
|
||||
<text class="product-name">{{item.name}}</text>
|
||||
<view class="product-meta">
|
||||
@ -46,7 +46,7 @@
|
||||
</view>
|
||||
|
||||
<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>
|
||||
</view>
|
||||
</view>
|
||||
@ -60,7 +60,7 @@
|
||||
<!-- 商品详情弹窗 -->
|
||||
<view class="product-modal" wx:if="{{showProductModal}}" bindtap="closeProductModal">
|
||||
<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">
|
||||
<text class="modal-name">{{currentProduct.name}}</text>
|
||||
<text class="modal-desc">{{currentProduct.description || '暂无描述'}}</text>
|
||||
|
||||
@ -33,7 +33,7 @@
|
||||
<text class="order-status status-{{item.status}}">{{getStatusText(item.status)}}</text>
|
||||
</view>
|
||||
<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">
|
||||
<text class="product-name">{{item.productName}}</text>
|
||||
<text class="store-name">{{item.storeName}}</text>
|
||||
@ -48,7 +48,7 @@
|
||||
</view>
|
||||
|
||||
<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>
|
||||
</view>
|
||||
|
||||
@ -67,7 +67,7 @@
|
||||
</view>
|
||||
|
||||
<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="points-used">使用积分: {{currentOrder.pointsUsed}}</text>
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
</view>
|
||||
|
||||
<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>
|
||||
</view>
|
||||
|
||||
|
||||
@ -17,13 +17,13 @@
|
||||
</view>
|
||||
</view>
|
||||
<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 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>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@ -6,7 +6,9 @@ Page({
|
||||
userInfo: null,
|
||||
ladderUser: null,
|
||||
currentStore: null,
|
||||
showQrcode: false
|
||||
showQrcode: false,
|
||||
needProfile: false,
|
||||
tempUserProfile: null
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
@ -18,11 +20,18 @@ Page({
|
||||
},
|
||||
|
||||
async initData() {
|
||||
if (!app.globalData.token) {
|
||||
return
|
||||
// 先进行微信登录获取openid
|
||||
if (!app.globalData.wxLoginInfo) {
|
||||
try {
|
||||
await app.wxLogin()
|
||||
} catch (e) {
|
||||
console.error('微信登录失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
if (app.globalData.token) {
|
||||
await this.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 {
|
||||
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()
|
||||
|
||||
this.setData({
|
||||
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' })
|
||||
} 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() {
|
||||
if (!this.data.userInfo?.memberCode) return
|
||||
|
||||
|
||||
@ -2,21 +2,39 @@
|
||||
<view class="container">
|
||||
<!-- 用户信息卡片 -->
|
||||
<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">
|
||||
<text class="nickname">{{userInfo.nickname || '新用户'}}</text>
|
||||
<view class="member-code" bindtap="showMemberCode">
|
||||
<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 class="user-header" wx:else bindtap="handleLogin">
|
||||
<image class="avatar-large" src="/images/avatar-default.png" mode="aspectFill"></image>
|
||||
<view class="user-meta">
|
||||
<text class="nickname">点击登录</text>
|
||||
|
||||
<!-- 未登录状态 -->
|
||||
<view class="login-panel" wx:else>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<!-- 积分展示 -->
|
||||
@ -50,31 +68,31 @@
|
||||
</view>
|
||||
|
||||
<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>
|
||||
</view>
|
||||
|
||||
<!-- 功能菜单 -->
|
||||
<view class="menu-list">
|
||||
<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>
|
||||
<image class="arrow" src="/images/icon-arrow.png" mode="aspectFit"></image>
|
||||
<image class="arrow" src="/images/icon-arrow.svg" mode="aspectFit"></image>
|
||||
</view>
|
||||
<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>
|
||||
<image class="arrow" src="/images/icon-arrow.png" mode="aspectFit"></image>
|
||||
<image class="arrow" src="/images/icon-arrow.svg" mode="aspectFit"></image>
|
||||
</view>
|
||||
<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>
|
||||
<image class="arrow" src="/images/icon-arrow.png" mode="aspectFit"></image>
|
||||
<image class="arrow" src="/images/icon-arrow.svg" mode="aspectFit"></image>
|
||||
</view>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
@ -21,6 +21,61 @@
|
||||
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 {
|
||||
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));
|
||||
}
|
||||
|
||||
// 查找或创建基础用户
|
||||
let user = await User.findOne({ where: { phone } });
|
||||
if (!user) {
|
||||
// 手机号对应的用户不存在,无法创建天梯用户
|
||||
return res.status(400).json(error('该手机号用户未注册小程序,请先引导用户注册', 400));
|
||||
}
|
||||
// 查找已注册的微信用户(可能不存在)
|
||||
const user = await User.findOne({ where: { phone } });
|
||||
|
||||
// 创建天梯用户
|
||||
// 创建天梯用户(允许user_id为空,待微信用户登录后自动关联)
|
||||
const ladderUser = await LadderUser.create({
|
||||
user_id: user.id,
|
||||
user_id: user ? user.id : null, // 如果微信用户存在则关联,否则为空
|
||||
store_id: targetStoreId,
|
||||
real_name,
|
||||
phone,
|
||||
@ -116,7 +112,11 @@ class LadderAdminController {
|
||||
status: 1
|
||||
});
|
||||
|
||||
res.json(success({ id: ladderUser.id }, '创建成功'));
|
||||
const message = user
|
||||
? '创建成功,已关联微信用户'
|
||||
: '创建成功,待用户注册小程序后自动关联';
|
||||
|
||||
res.json(success({ id: ladderUser.id, linked: !!user }, message));
|
||||
} catch (err) {
|
||||
console.error('创建天梯用户失败:', err);
|
||||
res.status(500).json(error('创建失败'));
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
const axios = require('axios');
|
||||
const crypto = require('crypto');
|
||||
const { User, LadderUser, Store, Match, MatchGame } = require('../models');
|
||||
const { generateMemberCode, success, error, calculateDistance } = require('../utils/helper');
|
||||
const { Op } = require('sequelize');
|
||||
|
||||
class UserController {
|
||||
// 微信登录
|
||||
// 微信登录(获取 session_key,用于后续手机号解密)
|
||||
async login(req, res) {
|
||||
try {
|
||||
const { code } = req.body;
|
||||
@ -14,7 +15,7 @@ class UserController {
|
||||
return res.status(400).json(error('缺少登录code', 400));
|
||||
}
|
||||
|
||||
// 获取微信openid
|
||||
// 获取微信openid和session_key
|
||||
const wxRes = await axios.get('https://api.weixin.qq.com/sns/jscode2session', {
|
||||
params: {
|
||||
appid: process.env.WX_APPID,
|
||||
@ -28,7 +29,92 @@ class UserController {
|
||||
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 } });
|
||||
@ -38,10 +124,27 @@ class UserController {
|
||||
user = await User.create({
|
||||
openid,
|
||||
unionid,
|
||||
phone,
|
||||
member_code: generateMemberCode(),
|
||||
nickname: '新用户',
|
||||
nickname: nickname || '新用户',
|
||||
avatar: avatar || '',
|
||||
gender: gender || 0,
|
||||
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
|
||||
@ -51,6 +154,12 @@ class UserController {
|
||||
{ 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({
|
||||
token,
|
||||
userInfo: {
|
||||
@ -60,15 +169,84 @@ class UserController {
|
||||
phone: user.phone,
|
||||
gender: user.gender,
|
||||
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) {
|
||||
console.error('登录失败:', err);
|
||||
console.error('手机号登录失败:', err);
|
||||
res.status(500).json(error('登录失败'));
|
||||
}
|
||||
}
|
||||
|
||||
// 解密微信手机号
|
||||
decryptPhoneNumber(sessionKey, encryptedData, iv) {
|
||||
const sessionKeyBuffer = Buffer.from(sessionKey, 'base64');
|
||||
const encryptedDataBuffer = Buffer.from(encryptedData, 'base64');
|
||||
const ivBuffer = Buffer.from(iv, 'base64');
|
||||
|
||||
const 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) {
|
||||
try {
|
||||
|
||||
@ -9,8 +9,8 @@ const LadderUser = sequelize.define('LadderUser', {
|
||||
},
|
||||
user_id: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: false,
|
||||
comment: '关联用户ID'
|
||||
allowNull: true,
|
||||
comment: '关联用户ID(可为空,待微信用户登录后通过手机号关联)'
|
||||
},
|
||||
store_id: {
|
||||
type: DataTypes.BIGINT,
|
||||
|
||||
@ -3,9 +3,15 @@ const router = express.Router();
|
||||
const userController = require('../controllers/userController');
|
||||
const { authUser } = require('../middlewares/auth');
|
||||
|
||||
// 微信登录
|
||||
// 微信登录(获取openid和session_key)
|
||||
router.post('/login', userController.login);
|
||||
|
||||
// 手机号授权登录(完成注册/登录)
|
||||
router.post('/phone-login', userController.phoneLogin);
|
||||
|
||||
// 更新用户资料(头像、昵称)
|
||||
router.put('/profile', authUser, userController.updateProfile);
|
||||
|
||||
// 获取用户信息
|
||||
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();
|
||||