322dceef by lttnew

密码

1 parent 4377830a
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is a **uni-app** (Vue 3) WeChat mini-program project for the **China Taekwondo Association (中国跆拳道协会)** membership management system.
- **Platform**: mp-weixin (WeChat Mini-Program)
- **Vue Version**: Vue 3 with `<script setup>` syntax
- **State Management**: Pinia
- **UI Framework**: uni-ui (components in `uni_modules/`)
- **Key Dependencies**: dayjs, underscore, lodash, crypto-js
## Development Commands
### Running the Project
- Use **HBuilderX** to open this project
- Or use CLI: `npm run dev:mp-weixin`
- Build: `npm run build:mp-weixin`
### Key Configurations
- `config.js` - API base URLs (dev/prod switching)
- `pages.json` - Page routing and global settings
- `manifest.json` - App configuration (appid, platform settings)
## Architecture
### Directory Structure
```
├── common/ # Shared utilities
│ ├── api.js # API functions (imported as @/common/api.js)
│ ├── request.js # HTTP request wrapper
│ ├── login.js # Login logic
│ ├── utils.js # Utility functions
│ └── pay.js # Payment logic
├── config.js # Environment config (API endpoints)
├── pages/ # Main package pages (index, exam, invoice, rank, webview)
├── login/ # Login sub-package
├── personal/ # Personal member sub-package
├── personalVip/ # VIP member sub-package
├── group/ # Group/Organization sub-package
├── level/ # Level examination sub-package
├── myCenter/ # User center sub-package
├── uni_modules/ # uni-ui components
└── static/ # Static assets
```
### API Pattern
All API functions are defined in `common/api.js` using a consistent pattern:
```js
export function apiFunctionName(data) {
return request({
url: '/path/to/endpoint',
method: 'post', // or 'get', 'put', 'delete'
params: data
})
}
```
### Page Sub-packages
Pages are organized into sub-packages defined in `pages.json` to optimize loading. Each sub-package has its own root directory (e.g., `level/`, `myCenter/`).
### Component Auto-import
uni-ui components are auto-imported via easycom in `pages.json`:
```json
"easycom": {
"autoscan": true,
"custom": {
"^uni-(.*)": "uni_modules/uni-$1/components/uni-$1/uni-$1.vue"
}
}
```
### Global Data
`app.globalData` contains shared state (from `App.vue`):
- `memberInfo` - Current member info
- `isLogin` - Login status
- `deptType` - Department type
Access via: `const app = getApp(); app.globalData.memberInfo`
### Form Handling Pattern
For forms with `uni-forms`, use `Object.assign(form.value, data)` instead of `form.value = data` to preserve reactive bindings.
### DateTime Picker
`uni-datetime-picker` requires ISO format strings or timestamps for v-model values.
### File Upload Pattern
Use `api.uploadFile(e)` or `api.uploadImg(e)` from `common/api.js` which return `{ code, msg }` - the file URL is in `res.msg`.
## Common Issues
### uni-data-select not working
If `uni-data-select` dropdowns don't appear, ensure:
1. easycom is properly configured in `pages.json`
2. The component is not blocked by CSS `overflow: hidden` on parent containers
### Responsive Data Binding
When updating form data from API responses, use `Object.assign()` to maintain Vue 3 reactivity:
```js
// Instead of: form.value = res.data
Object.assign(form.value, res.data)
```
......@@ -61,7 +61,14 @@ function getCodeImg() {
method: 'get'
})
}
function getSmsCodeImg(data) {
return request({
url: '/captchaSmsWithCaptchaImage',
// url: '/captchaSmsWithCaptchaImageForMiniApp',
method: 'post',
params: data
})
}
// 代退图形认证的获取手机验证码
function getSmsCode(data) {
return request({
......@@ -241,6 +248,7 @@ export {
pcLogin,
getCodeImg,
getSmsCode,
getSmsCodeImg,
h5Login,
h5LoginAuto,
loginByPhone,
......
......@@ -4,13 +4,13 @@ page {
background: #ecf0f6;
}
/* uni-data-checkbox 选中色全局覆盖为红色 */
uni-data-checkbox .checklist-box.is-checked {
border-color: #C4121B !important;
background-color: #C4121B !important;
}
uni-data-checkbox .checklist-box.is-checked .checklist-text {
color: #fff !important;
}
// uni-data-checkbox .checklist-box.is-checked {
// border-color: #C4121B !important;
// background-color: #C4121B !important;
// }
// uni-data-checkbox .checklist-box.is-checked .checklist-text {
// color: #fff !important;
// }
.wBox{box-sizing: border-box;}
.h3 {font-weight: bold;line-height: 2;}
.text-center{text-align: center;}
......
......@@ -933,7 +933,7 @@
padding: 20rpx 0;
background: #fff;
border-top: 1rpx solid #f0f0f0;
z-index: 999;
z-index: 98;
.btn-red {
background: linear-gradient(135deg, #AD181F 0%, #c42a2a 100%);
......
......@@ -68,7 +68,7 @@ import {
} from 'vue'
import {
getCodeImg,
getSmsCode,
getSmsCodeImg,
groupMemberRegister
} from '@/common/login.js'
import config from '@/config.js'
......@@ -113,6 +113,14 @@ function register() {
})
return
}
// 密码强度校验:8~18位大小写字母加数字加特殊符号组合
if (!validPassword(registerForm.value.password)) {
uni.showToast({
title: '密码必须为8~18位大小写字母、数字和特殊符号组合',
icon: 'none'
})
return
}
if (registerForm.value.password != registerForm.value.password2) {
uni.showToast({
title: '两次密码不一致,请重新输入',
......@@ -131,13 +139,27 @@ function register() {
groupMemberRegister(registerForm.value)
.then((res) => {
uni.showToast({
title: `恭喜你,您的账号 ${registerForm.value.telNo} 注册成功!`
title: `恭喜你,您的账号 ${registerForm.value.telNo} 注册成功!`,
icon: 'none'
})
registerForm.value = {}
setTimeout(goLogin, 2000)
})
}
// 密码校验:8~18位大小写字母加数字加特殊符号组合
function validPassword(pwd) {
if (!pwd || pwd.length < 8 || pwd.length > 18) {
return false
}
const lowerRegex = /[a-z]+/
const upperRegex = /[A-Z]+/
const digitRegex = /[0-9]+/
const symbolRegex = /[\W_]+/
const specific = /.*[~!@#$%^&*()_+`\-={}:";'<>?,.\/].*/
return (lowerRegex.test(pwd) && upperRegex.test(pwd) && digitRegex.test(pwd) && symbolRegex.test(pwd) && specific.test(pwd))
}
function goLogin() {
let path = '/login/loginC';
uni.navigateTo({
......@@ -169,7 +191,7 @@ function getCaptchaSms() {
return
}
getSmsCode({
getSmsCodeImg({
uuid: registerForm.value.uuid,
telNo: registerForm.value.telNo,
code: registerForm.value.captcha
......
......@@ -264,10 +264,10 @@
getTree()
form.value.deptType = res.data.dept.deptType
form.value.parentId = form.value.parentId.toString()
creditCode.value = form.value.creditCode
companyName.value = form.value.companyName
belongProvinceId.value = form.value.belongProvinceId
parentId.value = form.value.parentId
// creditCode.value = form.value.creditCode
// form.value.companyName = res.data.memberInfo.companyName
// belongProvinceId.value = form.value.belongProvinceId
// parentId.value = form.value.parentId
if (form.value.regionId) {
form.value.coordinates1 = form.value.regionId
......
{
"easycom": {
"autoscan": true,
"custom": {
"^uni-(.*)": "uni_modules/uni-$1/components/uni-$1/uni-$1.vue"
}
},
"pages": [{
"path": "pages/index/index"
},
......
......@@ -22,7 +22,9 @@
<uni-easyinput class="input-with-border" v-model="form.baseName" :disabled="!editIng" placeholder="单位名称" />
</uni-forms-item>
<uni-forms-item label="单位类型" required>
<uni-data-select :disabled="!editIng" v-model="form.type" :localdata="typeList"></uni-data-select>
<view style="width: 100%;">
<uni-data-select v-model="form.type" :localdata="typeList" placeholder="请选择单位类型"></uni-data-select>
</view>
</uni-forms-item>
<uni-forms-item label="联系人" required>
<uni-easyinput class="input-with-border" v-model="form.contact" :disabled="!editIng" placeholder="请输入联系人姓名" />
......@@ -51,10 +53,10 @@
<text v-if="authenticationStatusa == 4" class="text-warning">即将过期</text>
<text v-if="authenticationStatusa == 5" class="text-danger">已过期</text>
</view>
<view class="btn-row">
<!-- <view class="btn-row">
<button type="primary" :disabled="btn" @click="goPay">去缴费</button>
<button v-if="form.deptType != 2" type="default" @click="goAuditDetail">审核详情</button>
</view>
</view> -->
</view>
<uni-forms ref="certForm" :modelValue="form" label-width="90">
......@@ -100,7 +102,7 @@
<image :src="config.baseUrl_api + '/fs/static/yyzz@2x.png'" class="placeholder-img"/>
</view>
<view v-else class="license-preview">
<image v-if="typeof form.businessLicense === 'string'" :src="form.businessLicense" class="license-img" @click="previewImage(form.businessLicense)"></image>
<image :src="getImageUrl(getBusinessLicenseUrl())" class="license-img" @click="previewImage(getImageUrl(getBusinessLicenseUrl()))"></image>
<view class="delete-btn" v-if="editIng" @click="removeBusinessLicense">×</view>
</view>
</view>
......@@ -118,7 +120,7 @@
<image :src="config.baseUrl_api + '/fs/static/sfz_zm@2x.png'" class="placeholder-img"/>
</view>
<view v-else class="idcard-preview">
<image :src="form.legalIdcPhoto1" class="idcard-img" @click="previewImage(form.legalIdcPhoto1)"></image>
<image :src="getImageUrl(form.legalIdcPhoto1)" class="idcard-img" @click="previewImage(getImageUrl(form.legalIdcPhoto1))"></image>
<view class="delete-btn" v-if="editIng" @click="removeIdCardFront">×</view>
</view>
</view>
......@@ -129,7 +131,7 @@
<image :src="config.baseUrl_api + '/fs/static/sfz_fm@2x.png'" class="placeholder-img"/>
</view>
<view v-else class="idcard-preview">
<image :src="form.legalIdcPhoto2" class="idcard-img" @click="previewImage(form.legalIdcPhoto2)"></image>
<image :src="getImageUrl(form.legalIdcPhoto2)" class="idcard-img" @click="previewImage(getImageUrl(form.legalIdcPhoto2))"></image>
<view class="delete-btn" v-if="editIng" @click="removeIdCardBack">×</view>
</view>
</view>
......@@ -147,7 +149,7 @@
<image :src="config.baseUrl_api + '/fs/static/jgzp@2x.png'" class="placeholder-img"/>
</view>
<view v-else class="pictures-preview">
<image :src="form.pictures.split(',')[0]" class="picture-img" @click="previewImage(form.pictures.split(','))"></image>
<image :src="getImageUrl(form.pictures.split(',')[0])" class="picture-img" @click="previewImage(form.pictures.split(',').map(url => getImageUrl(url)))"></image>
<view class="delete-btn" v-if="editIng" @click="removePictures">×</view>
</view>
</view>
......@@ -166,7 +168,8 @@
<script setup>
import {
ref,
reactive
reactive,
computed
} from 'vue';
import * as api from '@/common/api.js';
import {
......@@ -174,6 +177,7 @@
onShow
} from '@dcloudio/uni-app';
import config from '@/config.js'
// import uniDataSelect from '@/uni_modules/uni-data-select/components/uni-data-select/uni-data-select.vue'
const app = getApp();
const form = ref({
......@@ -195,6 +199,17 @@
text: '其他'
}])
// 类型索引
const typeIndex = computed(() => {
return typeList.value.findIndex(item => String(item.value) === String(form.value.type))
})
// 类型选择
function typeChange(e) {
const index = e.detail.value
form.value.type = typeList.value[index].value
}
// 地址选项
const options = ref([])
const regionOptions = ref([])
......@@ -210,7 +225,7 @@
}, {
title: '会员认证'
}])
const creditCode = ref('')
// 编辑状态
const editIng = ref(true);
......@@ -232,6 +247,28 @@
// 考点审核状态
const auditStatus = ref('0')
// 图片URL处理:如果不是http开头,拼接baseUrl_api
function getImageUrl(url) {
if (!url) return ''
if (url.indexOf('http') === 0) return url
return config.baseUrl_api + url
}
// 解析营业执照URL
function getBusinessLicenseUrl() {
if (!form.value.businessLicense) return ''
try {
const arr = JSON.parse(form.value.businessLicense)
if (Array.isArray(arr) && arr.length > 0) {
return arr[0].url || ''
}
} catch (e) {
// 如果不是JSON格式,可能是直接返回的URL
return form.value.businessLicense
}
return ''
}
onLoad(option => {
getTree()
if (app.globalData.isLogin) {
......@@ -260,6 +297,9 @@
// Object.assign(form.value, res.data.memberInfo)
// }
form.value = { ...res.data.dept, ...res.data.memberInfo }
if (form.value.type) {
form.value.type = String(form.value.type)
}
authenticationStatusa.value = res.data.authenticationStatus
result.value = res.data.result
......@@ -311,11 +351,11 @@
btn.value = !result.value
}
creditCode.value = form.value.creditCode
legal.value = form.value.legal
legalIdcCode.value = form.value.legalIdcCode
coordinates1.value = form.value.provinceId
adress.value = form.value.adress
// creditCode.value = form.value.creditCode
// legal.value = form.value.legal
// legalIdcCode.value = form.value.legalIdcCode
// coordinates1.value = form.value.provinceId
// adress.value = form.value.adress
form.value.name = form.value.baseName
})
}
......@@ -601,22 +641,18 @@
// 提交认证信息(100%对齐PC端入参格式)
function submitCertification() {
uni.showLoading({ title: '提交中...' })
api.editMyMemberCertifiedInfo({
let params = {
parentId: form.value.parentId,
creditCode: form.value.creditCode,
legal: form.value.legal,
businessLicense: JSON.stringify({
url: form.value.businessLicense,
name: form.value.businessLicenseName
}),
businessLicense: form.value.businessLicense,
pictures: form.value.pictures,
memId: form.value.memId,
id: form.value.deptId,
name: form.value.name,
regionId: form.value.regionId?.value,
cityId: form.value.cityId.value,
provinceId: form.value.provinceId.value,
cityId: form.value.cityId.value.toString(),
provinceId: form.value.provinceId.value.toString(),
adress: form.value.adress,
deptType: app.globalData.deptType,
legalIdcPhoto: [form.value.legalIdcPhoto1, form.value.legalIdcPhoto2].join(','),
......@@ -625,7 +661,11 @@
siteTel: form.value.siteTel,
companyName: form.value.companyName,
legalIdcCode: form.value.legalIdcCode
}).then(res => {
}
console.log(666,params)
// return
uni.showLoading({ title: '提交中...' })
api.editMyMemberCertifiedInfo(params).then(res => {
uni.hideLoading()
if (res.code === 200) {
uni.showToast({ title: '提交成功', duration: 1500, icon: 'success' })
......@@ -654,23 +694,41 @@
sourceType: ['album', 'camera'],
success: (res) => {
if (res.tempFilePaths && res.tempFilePaths.length > 0) {
form.value.businessLicense = res.tempFilePaths[0]
// 调用API识别营业执照
uni.uploadFile({
url: config.baseUrl_api + '/member/info/getBusinessLicense',
filePath: res.tempFilePaths[0],
name: 'pic',
header: {
'Authorization': uni.getStorageSync('token')
},
success: (uploadRes) => {
const data = JSON.parse(uploadRes.data)
if (data.code === 200 && data.data) {
form.value.creditCode = data.data.creditCode
form.value.companyName = data.data.companyName
form.value.businessLicenseName = data.data.name
uni.showLoading({ title: '上传中...' })
// 先上传文件
api.uploadFile(res).then(uploadRes => {
if (uploadRes.code !== 200) {
throw new Error('上传失败')
}
const url = uploadRes.msg
// 上传成功后调用OCR识别
return uni.uploadFile({
url: config.baseUrl_api + '/member/info/getBusinessLicense',
filePath: res.tempFilePaths[0],
name: 'pic',
header: {
'Authorization': uni.getStorageSync('token')
}
}).then(ocrRes => {
return { url, ocrRes }
})
}).then(({ url, ocrRes }) => {
uni.hideLoading()
const data = JSON.parse(ocrRes.data)
let name = '营业执照'
if (data.code === 200 && data.data) {
form.value.creditCode = data.data.creditCode
form.value.companyName = data.data.companyName
name = data.data.name || '营业执照'
}
// 存储为数组格式的JSON字符串
form.value.businessLicense = JSON.stringify([{
url: url,
name: name
}])
}).catch(() => {
uni.hideLoading()
uni.showToast({ title: '上传失败', icon: 'none' })
})
}
}
......@@ -692,7 +750,7 @@
form.value.legalIdcPhoto2 = ''
}
// 身份证上传(修复:正面调用OCR,和PC端逻辑一致)
// 身份证上传
function onIdCardFrontSelect() {
uni.chooseImage({
count: 1,
......@@ -700,8 +758,16 @@
sourceType: ['album', 'camera'],
success: (res) => {
if (res.tempFilePaths && res.tempFilePaths.length > 0) {
form.value.legalIdcPhoto1 = res.tempFilePaths[0]
uni.showLoading({ title: '上传中...' })
api.uploadImg(res).then(data => {
if (data.code === 200) {
form.value.legalIdcPhoto1 = data.msg
}
uni.hideLoading()
}).catch(() => {
uni.hideLoading()
uni.showToast({ title: '上传失败', icon: 'none' })
})
}
}
})
......@@ -714,19 +780,30 @@
sourceType: ['album', 'camera'],
success: (res) => {
if (res.tempFilePaths && res.tempFilePaths.length > 0) {
form.value.legalIdcPhoto2 = res.tempFilePaths[0]
extractIdCardInfo() // 修复:正面上传后调用OCR提取信息
const tempPath = res.tempFilePaths[0]
uni.showLoading({ title: '上传中...' })
api.uploadImg(res).then(data => {
if (data.code === 200) {
form.value.legalIdcPhoto2 = data.msg
// 用临时文件路径调用OCR
extractIdCardInfo(tempPath)
}
uni.hideLoading()
}).catch(() => {
uni.hideLoading()
uni.showToast({ title: '上传失败', icon: 'none' })
})
}
}
})
}
// 提取身份证信息(修复:传正面照片legalIdcPhoto1,和PC端逻辑一致)
function extractIdCardInfo() {
if (form.value.legalIdcPhoto1) {
// 提取身份证信息
function extractIdCardInfo(tempPath) {
if (tempPath) {
uni.uploadFile({
url: config.baseUrl_api + '/person/info/getPersonInfoFromCert/0',
filePath: form.value.legalIdcPhoto2,
filePath: tempPath,
name: 'pic',
header: {
'Authorization': uni.getStorageSync('token')
......@@ -750,7 +827,19 @@
sourceType: ['album', 'camera'],
success: (res) => {
if (res.tempFilePaths && res.tempFilePaths.length > 0) {
form.value.pictures = res.tempFilePaths.join(',')
uni.showLoading({ title: '上传中...' })
// 循环上传多张图片
const promises = res.tempFilePaths.map(path => {
return api.uploadImg({ tempFilePaths: [path] })
})
Promise.all(promises).then(results => {
uni.hideLoading()
const urls = results.filter(r => r.code === 200).map(r => r.msg)
form.value.pictures = urls.join(',')
}).catch(() => {
uni.hideLoading()
uni.showToast({ title: '上传失败', icon: 'none' })
})
}
}
})
......@@ -951,6 +1040,7 @@
font-size: 28rpx;
color: #333;
margin-right: 10rpx;
width: 100px;
}
.btn-row {
......@@ -1146,7 +1236,7 @@
padding: 0 20rpx;
width: 100%;
box-sizing: border-box;
:deep(.uni-easyinput__input) {
white-space: nowrap;
overflow: hidden;
......@@ -1154,6 +1244,34 @@
}
}
/* picker 样式 */
.picker-view {
height: 70rpx;
line-height: 70rpx;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20rpx;
box-sizing: border-box;
}
.picker-text {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.arrow-icon {
font-size: 20rpx;
color: #999;
}
.placeholder {
color: #999;
}
/* 级联选择器容器 - 修复边框和宽度问题(删除重复定义) */
.picker-wrapper {
border: 1rpx solid #e0e0e0;
......@@ -1200,17 +1318,34 @@
min-height: 70rpx;
}
/* uni-data-picker 弹窗样式 - 列表文字大小 */
:deep(.item) {
font-size: 32rpx !important;
padding: 24rpx 30rpx !important;
}
:deep(.item-text) {
font-size: 32rpx !important;
}
:deep(.selected-item-text) {
font-size: 32rpx !important;
}
/* 修复uni-forms-item中内容超出问题 */
/* 暂时移除 overflow: hidden 以避免影响下拉组件 */
/*
:deep(.uni-forms-item__content) {
overflow: hidden;
width: 100%;
}
*/
/* 确保所有表单元素不超出父容器 */
/*
:deep(uni-forms-item) {
width: 100%;
overflow: hidden;
}
*/
/* 修复选择器容器宽度问题 */
.picker-wrapper {
......
......@@ -24,7 +24,7 @@
<view v-if="(item.progress && item.progress !== 100) ||item.progress===0 " class="file-picker__progress">
<progress class="file-picker__progress-item" :percent="item.progress === -1?0:item.progress" stroke-width="4"
:backgroundColor="item.errMsg?'#ff5a5f':'#EBEBEB'" />
</view>
</view>
<view v-if="item.status === 'error'" class="file-picker__mask" @click.stop="uploadFiles(item,index)">
点击重试
</view>
......@@ -69,10 +69,10 @@
borderStyle: {}
}
}
},
readonly:{
type:Boolean,
default:false
},
readonly:{
type:Boolean,
default:false
}
},
computed: {
......@@ -114,7 +114,7 @@
let classles = ''
for (let i in obj) {
classles += `${i}:${obj[i]};`
}
}
return classles
},
borderLineStyle() {
......@@ -133,19 +133,19 @@
} else {
width = width.indexOf('px') ? width : width + 'px'
}
obj['border-width'] = width
if (typeof style === 'number') {
style += 'px'
} else {
style = style.indexOf('px') ? style : style + 'px'
obj['border-width'] = width
if (typeof style === 'number') {
style += 'px'
} else {
style = style.indexOf('px') ? style : style + 'px'
}
obj['border-top-style'] = style
}
let classles = ''
for (let i in obj) {
classles += `${i}:${obj[i]};`
}
}
return classles
}
},
......@@ -176,9 +176,9 @@
</script>
<style lang="scss">
.uni-file-picker__files {
.uni-file-picker__files {
/* #ifndef APP-NVUE */
display: flex;
display: flex;
/* #endif */
flex-direction: column;
justify-content: flex-start;
......@@ -194,9 +194,9 @@
overflow: hidden;
}
.file-picker__mask {
.file-picker__mask {
/* #ifndef APP-NVUE */
display: flex;
display: flex;
/* #endif */
justify-content: center;
align-items: center;
......@@ -214,12 +214,12 @@
position: relative;
}
.uni-file-picker__item {
.uni-file-picker__item {
/* #ifndef APP-NVUE */
display: flex;
display: flex;
/* #endif */
align-items: center;
padding: 8px 10px;
padding: 18px 10px;
padding-right: 5px;
padding-left: 10px;
}
......@@ -232,17 +232,17 @@
flex: 1;
font-size: 14px;
color: #666;
margin-right: 25px;
margin-right: 25px;
/* #ifndef APP-NVUE */
word-break: break-all;
word-wrap: break-word;
word-wrap: break-word;
/* #endif */
}
.icon-files {
.icon-files {
/* #ifndef APP-NVUE */
position: static;
background-color: initial;
background-color: initial;
/* #endif */
}
......@@ -288,10 +288,10 @@
transform: rotate(90deg);
}
.icon-del-box {
.icon-del-box {
/* #ifndef APP-NVUE */
display: flex;
margin: auto 0;
margin: auto 0;
/* #endif */
align-items: center;
justify-content: center;
......@@ -322,4 +322,4 @@
}
/* #endif */
</style>
</style>
......
Styling with Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!