feat(user): 统一VIP概念为开发者并优化开发者登录及申请

- 将VIP相关文案、图标改为开发者,涉及用户中心、门店中心、审核页等
- 优化登录弹窗UI,改为红色系,提升视觉统一性和交互体验
- 关于我们页面品牌和公司信息内容更新,展示最新企业介绍和业务范围
- VIP升级页新增“所在城市”选择,使用微信原生地区选择器,完善地址信息
- 用户申请表单中新增城市字段及校验,提交时携带城市信息
- 优化审核页面文案,统称开发者身份,明确申请审核内容
- 修复UserRoleController.save接口bug,避免userId被无条件覆盖导致角色绑错
- 动态查询VIP角色ID,避免硬编码,支持多租户环境正确绑定角色
This commit is contained in:
2026-07-22 13:19:10 +08:00
parent 12d6bdfb06
commit 8d4b55098f
9 changed files with 221 additions and 102 deletions

View File

@@ -32,6 +32,8 @@ export interface ShopDealerApply {
updateTime?: string;
// 门店名称
merchantName?: string;
// 所在城市(省 市 区,以空格分隔)
city?: string;
// 门店地址
address?: string;
}

View File

@@ -45,9 +45,9 @@ const FEATURE_CARDS = [
},
{
key: 'vip-review',
icon: '👑',
title: 'VIP会员审核',
desc: '审核客户VIP会员申请',
icon: '🔰',
title: '开发者审核',
desc: '审核客户开发者身份的申请',
url: '/pages/user/vip-review/index',
color: '#7c3aed',
bgColor: '#ede9fe',

View File

@@ -132,23 +132,29 @@ const AboutPage: React.FC = () => {
{/* 头部品牌区 */}
<View className='bg-white py-10 flex flex-col items-center'>
<View className='w-20 h-20 rounded-2xl bg-gradient-to-br from-blue-400 to-blue-600 flex items-center justify-center shadow-lg mb-4'>
<Text className='text-white text-4xl font-bold'></Text>
<Text className='text-white text-4xl font-bold'></Text>
</View>
<Text className='text-xl font-bold text-gray-800'></Text>
<Text className='text-sm text-gray-400 mt-1'> v1.0.0</Text>
<Text className='text-xl font-bold text-gray-800'></Text>
<Text className='text-sm text-gray-400 mt-1'>websopy v1.0.0</Text>
</View>
{/* 公司简介 */}
{/* 公司简介 / 品牌故事 */}
<View className='bg-white mx-3 mt-3 rounded-xl p-4' style={{ boxShadow: '0 2px 8px rgba(0,0,0,0.04)' }}>
<Text className='text-base font-medium text-gray-800 mb-3 block'></Text>
<Text className='text-base font-medium text-gray-800 mb-3 block'></Text>
<Text className='text-sm text-gray-600 leading-relaxed'>
websopy宿
"让每一家企业
都能轻松用上专业的软件"使
</Text>
<Text className='text-sm text-gray-600 leading-relaxed mt-2'>
"诚信经营、服务至上"
便
"专业、务实、共赢"
</Text>
<Text className='text-sm text-gray-600 leading-relaxed mt-2'>
</Text>
</View>
@@ -156,19 +162,19 @@ const AboutPage: React.FC = () => {
<View className='bg-white mx-3 mt-3 rounded-xl p-4' style={{ boxShadow: '0 2px 8px rgba(0,0,0,0.04)' }}>
<Text className='text-base font-medium text-gray-800 mb-3 block'></Text>
{[
{ icon: '📺', title: '家电零售', desc: '电视、冰箱、洗衣机等全品类家电销售' },
{ icon: '❄️', title: '空调服务', desc: '空调销售、安装、加氟、清洗及维修' },
{ icon: '🍳', title: '厨房电器', desc: '油烟机、燃气灶、热水器等厨卫电器' },
{ icon: '🛠️', title: '安装维修', desc: '专业师傅上门安装、检测及故障维修' },
{ icon: '🔧', title: '保养维护', desc: '定期清洗保养,延长家电使用寿命' },
{ icon: '✅', title: '品质保障', desc: '正品货源、透明报价、售后无忧' },
{ icon: '🖥️', title: '企业管理软件', desc: 'ERP / CRM / OA 等数字化管理工具' },
{ icon: '🌐', title: '品牌官网建设', desc: '企业官网、品牌站、营销落地页定制' },
{ icon: '📱', title: '移动应用开发', desc: '微信小程序、跨端 APP 定制开发' },
{ icon: '🔗', title: '系统集成对接', desc: '第三方平台 API 对接与数据打通' },
{ icon: '📊', title: '数据智能分析', desc: '经营数据可视化与业务洞察' },
{ icon: '✅', title: '技术服务支持', desc: '部署运维、培训陪跑、售后无忧' },
].map((item, idx) => (
<View
key={idx}
className={`flex items-start gap-3 py-3 ${idx < 5 ? 'border-b border-gray-50' : ''}`}
>
<Text className='text-2xl mt-0.5'>{item.icon}</Text>
<View className='flex-1'>
<View className='flex-1 flex flex-col'>
<Text className='text-sm font-medium text-gray-700'>{item.title}</Text>
<Text className='text-xs text-gray-400 mt-0.5'>{item.desc}</Text>
</View>
@@ -181,10 +187,12 @@ const AboutPage: React.FC = () => {
<Text className='text-base font-medium text-gray-800 mb-3 block'></Text>
<View className='flex flex-col gap-2'>
{[
{ label: '经营部名称', value: '威硕谱软件' },
{ label: '经营地址', value: '大新里南718号' },
{ label: '联系电话', value: '18269229683' },
{ label: '营业时间', value: '9:00 ~ 18:00' },
{ label: '公司名称', value: '南宁市网宿信息科技有限公司' },
{ label: '品牌名称', value: '威数谱软件websopy' },
{ label: '公司总部', value: '南宁市良庆区五象大道401号五象航洋城总部基地1号楼1226室' },
{ label: '联系电话', value: '0771-5386339' },
{ label: '电子邮箱', value: 'websopy@163.com' },
{ label: '官方网站', value: 'websoft.top / websopy.com' },
].map((item, idx) => (
<View key={idx} className='flex items-start justify-between'>
<Text className='text-sm text-gray-500 flex-shrink-0'>{item.label}</Text>
@@ -196,78 +204,131 @@ const AboutPage: React.FC = () => {
{/* 底部版权 */}
<View className='flex flex-col items-center py-8' onClick={handleCopyrightClick}>
<Text className='text-xs text-gray-400'>Copyright © 2024 </Text>
<Text className='text-xs text-gray-400 mt-1'>All Rights Reserved</Text>
<Text className='text-xs text-gray-400'>Copyright © 2024 宿</Text>
<Text className='text-xs text-gray-400 mt-1'> websopy · All Rights Reserved</Text>
</View>
{/* 隐藏的开发者登录弹窗 */}
{showLogin && (
<View
className='fixed inset-0 z-[9999] flex items-center justify-center'
style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}
style={{ backgroundColor: 'rgba(0,0,0,0.55)' }}
onClick={handleCloseLogin}
catchMove
>
<View
className='rounded-2xl bg-white'
style={{ width: '340px', padding: '24px 20px' }}
className='rounded-3xl bg-white'
style={{ width: '320px', padding: '32px 24px' }}
onClick={(e) => e.stopPropagation()}
>
<View className='mb-3 flex items-center justify-between'>
<Text className='text-lg font-bold text-gray-800'></Text>
{/* 标题栏 */}
<View className='mb-2 flex items-center justify-between'>
<Text className='text-xl font-bold text-gray-800'></Text>
<Text
className='text-lg text-gray-400'
style={{ padding: '4px 8px' }}
className='text-xl text-gray-300 leading-none cursor-pointer active:opacity-60'
style={{ padding: '6px', lineHeight: 1 }}
onClick={handleCloseLogin}
>
</Text>
</View>
<Text className='mb-5 block text-xs text-gray-400'>
<Text className='mb-6 block text-xs text-gray-400 leading-relaxed'>
使+
</Text>
<View className='mb-3'>
{/* 手机号输入框 */}
<View className='mb-4'>
<Input
type='number'
placeholder='请输入手机号码'
maxLength={11}
value={formData.phone || ''}
onChange={(val) => setFormData({ ...formData, phone: val })}
style={{ backgroundColor: '#f5f5f5', borderRadius: '8px' }}
style={{
backgroundColor: '#fff5f5',
borderRadius: '12px',
height: '48px',
paddingLeft: '16px',
paddingRight: '16px',
fontSize: '15px',
border: '1.5px solid #e53e3e',
boxSizing: 'border-box',
}}
/>
</View>
<View className='mb-6 flex items-center bg-white rounded-lg pr-2'>
<Input
type='number'
placeholder='请输入6位验证码'
maxLength={6}
value={formData.code || ''}
onChange={(val) => setFormData({ ...formData, code: val })}
style={{ backgroundColor: '#f5f5f5', borderRadius: '8px' }}
/>
{/* 验证码输入 + 获取按钮 */}
<View className='mb-8 flex items-center gap-3'>
<View className='flex-1'>
<Input
type='number'
placeholder='请输入6位验证码'
maxLength={6}
value={formData.code || ''}
onChange={(val) => setFormData({ ...formData, code: val })}
style={{
backgroundColor: '#fff5f5',
borderRadius: '12px',
height: '48px',
paddingLeft: '16px',
paddingRight: '12px',
fontSize: '15px',
border: '1.5px solid #e53e3e',
boxSizing: 'border-box',
}}
/>
</View>
<Button
size='small'
type={countdown > 0 ? 'default' : 'primary'}
loading={sendingCode}
disabled={sendingCode || countdown > 0}
onClick={handleSendCode}
style={{
height: '48px',
minWidth: '110px',
borderRadius: '12px',
fontSize: '13px',
fontWeight: 500,
flexShrink: 0,
...(countdown > 0 ? {
backgroundColor: '#f0f0f0',
color: '#999',
} : {
backgroundColor: '#e53e3e',
borderColor: '#e53e3e',
}),
}}
>
{countdown > 0 ? `${countdown}s` : sendingCode ? '发送中' : '获取验证码'}
{countdown > 0 ? `${countdown}s 后重发` : sendingCode ? '发送中' : '获取验证码'}
</Button>
</View>
{/* 登录按钮 */}
<View className='flex justify-center'>
<Button
block
type='primary'
size='large'
className='w-full rounded-lg p-2'
loading={loading}
disabled={loading}
onClick={handleLogin}
style={{
height: '50px',
borderRadius: '14px',
fontSize: '17px',
fontWeight: 600,
letterSpacing: '2px',
background: loading
? '#c0c0c0'
: 'linear-gradient(135deg, #fc5c5c 0%, #e53e3e 100%)',
border: 'none',
boxShadow: loading
? 'none'
: '0 6px 20px rgba(229, 62, 62, 0.35)',
}}
>
{loading ? '登录中...' : '登录'}
{loading ? '登录中...' : '登 录'}
</Button>
</View>
</View>

View File

@@ -101,8 +101,8 @@ const UserPage: React.FC = () => {
})
const menuItems = [
// 升级VIP会员 - 醒目样式
{ icon: '👑', label: '升级VIP会员', url: '/pages/user/vip-upgrade/index', highlight: true, requireAuth: true },
// 申请成为开发者 - 醒目样式
{ icon: '🔰', label: '申请成为开发者', url: '/pages/user/vip-upgrade/index', highlight: true, requireAuth: true },
// 门店中心:仅门店店员/店长显示(通过 /shop/shop-store-user/my 判断)
...(storeInfo ? [{ icon: '🏪', label: '门店中心', url: '/pages/store/center/index', requireAuth: true }] : []),
{ icon: '💰', label: '我的钱包', url: '/pages/user/wallet', requireAuth: true },

View File

@@ -9,7 +9,7 @@ import { TenantId } from '@/config/app'
import { getMyClerk } from '@/api/shop/shopStoreUser'
definePageConfig({
navigationBarTitleText: 'VIP会员审核',
navigationBarTitleText: '开发者审核',
})
// Tab 配置
@@ -24,7 +24,7 @@ const TABS: { key: TabKey; label: string; status: number }[] = [
// VIP 角色信息(对应 system/role 表)
// 角色 ID 不写死,按 roleCode + 当前租户ID 动态查询
const VIP_ROLE_CODE = 'developer'
const VIP_ROLE_NAME = '认证开发者'
const VIP_ROLE_NAME = '开发者'
// 获取当前租户 ID登录时写入 storage未登录时回退到默认租户
function getCurrentTenantId(): number | string | undefined {
@@ -181,7 +181,7 @@ const VipReviewPage: React.FC = () => {
const handleApprove = (item: ShopDealerApply) => {
Taro.showModal({
title: '确认通过',
content: `确认通过「${item.realName || item.merchantName || '该用户'}」的VIP会员申请?\n\n请确认已实地考察核实门店名称和地址。`,
content: `确认通过「${item.realName || item.merchantName || '该用户'}」的开发者申请?\n\n请确认已实地考察核实开发团队名称与公司地址。`,
confirmColor: '#22c55e',
success: async (res) => {
if (!res.confirm) return
@@ -216,7 +216,7 @@ const VipReviewPage: React.FC = () => {
const handleReject = (item: ShopDealerApply) => {
Taro.showModal({
title: '驳回申请',
content: `确认驳回「${item.realName || item.merchantName || '该用户'}」的VIP会员申请?`,
content: `确认驳回「${item.realName || item.merchantName || '该用户'}」的开发者申请?`,
confirmColor: '#ef4444',
success: async (res) => {
if (!res.confirm) return
@@ -291,12 +291,12 @@ const VipReviewPage: React.FC = () => {
{renderStatusTag(item.applyStatus!)}
</View>
{/* 门店信息 */}
{/* 开发团队及公司信息 */}
<View className='bg-gray-50 rounded-lg p-3 mb-3'>
<View className='flex items-start gap-2 mb-2'>
<Text className='text-xs text-gray-400 mt-0.5'>🏪</Text>
<View className='flex-1'>
<Text className='text-xs text-gray-400 block'></Text>
<Text className='text-xs text-gray-400 block'></Text>
<Text className='text-sm text-gray-700 block mt-0.5'>
{item.merchantName || item.realName || '-'}
</Text>
@@ -305,7 +305,7 @@ const VipReviewPage: React.FC = () => {
<View className='flex items-start gap-2'>
<Text className='text-xs text-gray-400 mt-0.5'>📍</Text>
<View className='flex-1'>
<Text className='text-xs text-gray-400 block'></Text>
<Text className='text-xs text-gray-400 block'></Text>
<Text className='text-sm text-gray-700 block mt-0.5'>
{item.address || '-'}
</Text>
@@ -371,7 +371,7 @@ const VipReviewPage: React.FC = () => {
<View className='flex-1'>
<Text className='text-xs text-green-700 block font-medium'></Text>
<Text className='text-xs text-green-600 block mt-1 leading-relaxed'>
VIP会员
</Text>
</View>
</View>

View File

@@ -1,5 +1,5 @@
export default {
navigationBarTitleText: '升级VIP会员',
navigationBarTitleText: '申请成为开发者',
enableShareAppMessage: true,
enableShareTimeline: true,
}

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react'
import { View, Text, Input } from '@tarojs/components'
import { View, Text, Input, Picker } from '@tarojs/components'
import Taro from '@tarojs/taro'
import { useUser } from '@/hooks/useUser'
import { useShare } from '@/hooks/useShare'
@@ -7,7 +7,7 @@ import { addShopDealerApply, listShopDealerApply } from '@/api/shop/shopDealerAp
import type { ShopDealerApply } from '@/api/shop/shopDealerApply/model'
definePageConfig({
navigationBarTitleText: '升级VIP会员',
navigationBarTitleText: '申请成为开发者',
enableShareAppMessage: true,
enableShareTimeline: true,
})
@@ -16,11 +16,14 @@ const VipUpgradePage: React.FC = () => {
const { user, isLoggedIn } = useUser()
// 分享 / 朋友圈
useShare({
title: '升级VIP会员,享专属优惠价',
title: '成为认证开发者,享专属权益',
path: '/pages/user/vip-upgrade',
})
const [realName, setRealName] = useState('')
const [address, setAddress] = useState('')
// 所在城市(省市区三级,使用微信原生地区选择器)
const [cityValue, setCityValue] = useState<string[]>([])
const cityText = cityValue.join(' ')
const [loading, setLoading] = useState(false)
const [existingApply, setExistingApply] = useState<ShopDealerApply | null>(null)
@@ -30,7 +33,7 @@ const VipUpgradePage: React.FC = () => {
return
}
if (user) {
// 门店名称和地址统一从 shop-dealer-apply 表读取
// 开发团队名称与公司地址统一从 shop-dealer-apply 表读取
listShopDealerApply({ userId: (user as any)?.userId || (user as any)?.id })
.then(data => {
if (!data || data.length === 0) return
@@ -40,6 +43,7 @@ const VipUpgradePage: React.FC = () => {
setExistingApply(latest)
setRealName(latest.realName || '')
setAddress(latest.address || '')
setCityValue(latest.city ? String(latest.city).split(' ').filter(Boolean) : [])
}
})
.catch(() => {})
@@ -48,11 +52,15 @@ const VipUpgradePage: React.FC = () => {
const handleSave = async () => {
if (!realName.trim()) {
Taro.showToast({ title: '请输入门店名称', icon: 'none' })
Taro.showToast({ title: '请输入开发团队名称', icon: 'none' })
return
}
if (!cityText) {
Taro.showToast({ title: '请选择所在城市', icon: 'none' })
return
}
if (!address.trim()) {
Taro.showToast({ title: '请输入门店地址', icon: 'none' })
Taro.showToast({ title: '请输入公司地址', icon: 'none' })
return
}
setLoading(true)
@@ -60,6 +68,7 @@ const VipUpgradePage: React.FC = () => {
await addShopDealerApply({
userId: (user as any)?.userId || (user as any)?.id,
realName: realName.trim(),
city: cityText,
address: address.trim(),
applyType: 10, // 需后台审核
applyStatus: 10, // 待审核
@@ -91,11 +100,11 @@ const VipUpgradePage: React.FC = () => {
<View className='absolute -bottom-6 -left-6 w-24 h-24 rounded-full opacity-15' style={{ background: 'radial-gradient(circle, #ffffff, transparent)' }} />
<View className='relative z-10 flex flex-col items-center'>
<View className='w-16 h-16 rounded-full bg-white bg-opacity-20 flex items-center justify-center mb-3'>
<Text className='text-3xl'>👑</Text>
<Text className='text-3xl'>🔰</Text>
</View>
<Text className='text-white text-xl font-bold'>VIP会员</Text>
<Text className='text-white text-xl font-bold'></Text>
<Text className='text-white text-opacity-80 text-sm mt-1'>
{existingApply?.applyStatus === 20 ? '恭喜您已是VIP会员' : '填写门店信息申请VIP会员资格'}
{existingApply?.applyStatus === 20 ? '恭喜您已成为开发者' : '填写开发团队及公司地址,申请成为开发者身份'}
</Text>
</View>
</View>
@@ -115,8 +124,8 @@ const VipUpgradePage: React.FC = () => {
</Text>
<Text className='text-gray-500 text-xs mt-1 block leading-relaxed'>
{existingApply.applyStatus === 10
? '您的VIP申请正在审核中,请耐心等待。审核结果将通过消息通知您。'
: '恭喜!您的VIP会员申请已审核通过,现在可以享受全部VIP专属权益。'}
? '您的开发者申请正在审核中,请耐心等待。审核结果将通过消息通知您。'
: '恭喜!您的开发者申请已审核通过,现在可以享受全部开发者专属权益。'}
</Text>
</View>
</View>
@@ -133,7 +142,7 @@ const VipUpgradePage: React.FC = () => {
<View className='flex-1'>
<Text className='text-gray-800 text-sm font-medium block'></Text>
<Text className='text-gray-500 text-xs mt-1 block leading-relaxed'>
VIP会员专属权
</Text>
</View>
</View>
@@ -142,12 +151,12 @@ const VipUpgradePage: React.FC = () => {
{/* 表单 */}
<View className={`${existingApply ? 'mt-3' : ''} mx-3 mt-3 bg-white rounded-xl overflow-hidden`}>
{/* 门店名称 */}
{/* 开发团队 */}
<View className='px-4 py-4 border-b border-gray-50'>
<Text className='text-sm text-gray-700 font-medium block mb-2'> <Text className='text-red-500'>*</Text></Text>
<Text className='text-sm text-gray-700 font-medium block mb-2'> <Text className='text-red-500'>*</Text></Text>
<Input
className='w-full text-sm text-gray-800 bg-gray-50 rounded-lg px-3 py-3'
placeholder='请输入您的门店名称'
placeholder='请输入您的开发团队名称'
value={realName}
onInput={(e: any) => setRealName(e.detail.value)}
maxlength={50}
@@ -155,12 +164,25 @@ const VipUpgradePage: React.FC = () => {
/>
</View>
{/* 门店地址 */}
{/* 所在城市 */}
<Picker mode='region' value={cityValue} onChange={(e: any) => setCityValue(e.detail.value || [])} disabled={isReadonly}>
<View className='px-4 py-4 border-b border-gray-50'>
<Text className='text-sm text-gray-700 font-medium block mb-2'> <Text className='text-red-500'>*</Text></Text>
<View className='w-full text-sm bg-gray-50 rounded-lg px-3 py-3 flex items-center justify-between'>
<Text className={cityText ? 'text-gray-800' : 'text-gray-400'}>
{cityText || '请选择所在城市'}
</Text>
<Text className='text-gray-300 text-xs'></Text>
</View>
</View>
</Picker>
{/* 公司地址 */}
<View className='px-4 py-4'>
<Text className='text-sm text-gray-700 font-medium block mb-2'> <Text className='text-red-500'>*</Text></Text>
<Text className='text-sm text-gray-700 font-medium block mb-2'> <Text className='text-red-500'>*</Text></Text>
<Input
className='w-full text-sm text-gray-800 bg-gray-50 rounded-lg px-3 py-3'
placeholder='请输入您的门店详细地址'
placeholder='请输入您的办公地址'
value={address}
onInput={(e: any) => setAddress(e.detail.value)}
maxlength={100}
@@ -169,15 +191,15 @@ const VipUpgradePage: React.FC = () => {
</View>
</View>
{/* VIP权益预览 */}
{/* 开发者权益预览 */}
<View className='mx-3 mt-4'>
<Text className='text-base font-medium text-gray-800 mb-3 block'>VIP会员专属权</Text>
<Text className='text-base font-medium text-gray-800 mb-3 block'></Text>
<View className='grid grid-cols-2 gap-3'>
{[
{ icon: '💎', title: '更高佣金', desc: '享受高额团队佣金' },
{ icon: '🎁', title: '专属折扣', desc: '购物享VIP专属价' },
{ icon: '🎁', title: '专属折扣', desc: '购物享开发者专属价' },
{ icon: '⚡', title: '优先发货', desc: '订单优先处理配送' },
{ icon: '🎧', title: '专属客服', desc: '一对一VIP服务' },
{ icon: '🎧', title: '专属客服', desc: '一对一开发者服务' },
].map(item => (
<View key={item.title} className='bg-white rounded-xl p-4 flex items-center gap-3'>
<View className='w-10 h-10 rounded-full bg-green-50 flex items-center justify-center flex-shrink-0'>