refactor(user): 将VIP身份统一改为开发者标记

- 审核通过时不再新增角色,而是更新用户isDeveloper布尔标记
- 后端UserService新增markAsDeveloper方法,通过updateById设置isDeveloper
- UserController新增PUT接口更新开发者标记,权限使用sys:userRole:save
- 前端api新增setUserDeveloper调用新的审核接口
- vip-review页面移除角色相关代码,改为调用setUserDeveloper方法
- 约定开发者身份即sys_user.is_developer字段,不再依赖角色表记录
- app.config.ts追加开发者中心与平台管理相关页面路由
- 新增开发者中心、平台管理的多个API模块(apikey、appProduct、appDomain、developer请求等)
- 添加公共工具方法和复用组件用于开发者中心及平台管理功能
- 统一品牌名称为“威数谱软件”,更新文案及界面展现一致性
This commit is contained in:
2026-07-22 14:57:03 +08:00
parent 2ea16e22cf
commit 39d3b59434
24 changed files with 2015 additions and 95 deletions

View File

@@ -0,0 +1,175 @@
import React, { useState, useEffect, useCallback } from 'react'
import { View, Text, ScrollView, Input, Textarea } from '@tarojs/components'
import Taro from '@tarojs/taro'
import NavBar from '@/components/NavBar'
import Loading from '@/components/common/Loading'
import { useUser } from '@/hooks/useUser'
import { pageAppApiKey, createAppApiKey, updateAppApiKeyStatus, removeAppApiKey } from '@/api/app/apikey'
import { getPageList, maskKey } from '@/utils/devcenter'
definePageConfig({ navigationBarTitleText: 'API Key 管理' })
const ApiKeysPage: React.FC = () => {
const { isLoggedIn } = useUser()
const [loading, setLoading] = useState(true)
const [list, setList] = useState<any[]>([])
const [showModal, setShowModal] = useState(false)
const [form, setForm] = useState({ name: '', remark: '' })
const [submitting, setSubmitting] = useState(false)
const load = useCallback(async () => {
setLoading(true)
try {
const data = await pageAppApiKey({ page: 1, limit: 50 })
setList(getPageList(data))
} catch (e) {
// ignore
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
if (isLoggedIn) load()
}, [isLoggedIn, load])
const toggle = async (item: any) => {
try {
await updateAppApiKeyStatus(item.id, item.status === 0 ? 1 : 0)
Taro.showToast({ title: '已更新', icon: 'none' })
load()
} catch (e: any) {
Taro.showToast({ title: e?.message || '操作失败', icon: 'none' })
}
}
const remove = (item: any) => {
Taro.showModal({
title: '删除密钥',
content: '删除后无法恢复,确定删除?',
success: async (r) => {
if (r.confirm) {
try {
await removeAppApiKey(item.id)
Taro.showToast({ title: '已删除', icon: 'success' })
load()
} catch (e: any) {
Taro.showToast({ title: e?.message || '删除失败', icon: 'none' })
}
}
},
})
}
const copy = (item: any) => {
Taro.setClipboardData({ data: item.apiKey || '' })
}
const submit = async () => {
if (!form.name.trim()) {
Taro.showToast({ title: '请填写名称', icon: 'none' })
return
}
setSubmitting(true)
try {
await createAppApiKey({ name: form.name, scopes: 'read,write', remark: form.remark })
Taro.showToast({ title: '创建成功', icon: 'success' })
setShowModal(false)
load()
} catch (e: any) {
Taro.showToast({ title: e?.message || '创建失败', icon: 'none' })
} finally {
setSubmitting(false)
}
}
return (
<View className='min-h-screen bg-gray-100'>
<NavBar
title='API Key 管理'
rightText='创建'
onRightClick={() => {
setForm({ name: '', remark: '' })
setShowModal(true)
}}
/>
<ScrollView scrollY>
{loading ? (
<Loading />
) : list.length === 0 ? (
<View className='flex flex-col items-center justify-center py-24'>
<Text className='text-gray-400 text-sm'> API Key</Text>
</View>
) : (
<View className='mt-3'>
{list.map((item) => (
<View key={item.id} className='bg-white px-4 py-3 border-b border-gray-50'>
<View className='flex items-center justify-between'>
<Text className='text-sm text-gray-800'>{item.name}</Text>
<Text
className='text-xs px-1.5 py-0.5 rounded-full'
style={{
color: item.status === 0 ? '#16a34a' : '#6b7280',
background: (item.status === 0 ? '#16a34a' : '#6b7280') + '22',
}}
>
{item.status === 0 ? '正常' : '禁用'}
</Text>
</View>
<Text className='text-xs text-gray-400 mt-1 block'>{maskKey(item.apiKey)}</Text>
<View className='flex gap-4 mt-2'>
<Text className='text-xs' style={{ color: '#2563eb' }} onClick={() => copy(item)}>
</Text>
<Text className='text-xs' style={{ color: '#d97706' }} onClick={() => toggle(item)}>
{item.status === 0 ? '禁用' : '启用'}
</Text>
<Text className='text-xs' style={{ color: '#dc2626' }} onClick={() => remove(item)}>
</Text>
</View>
</View>
))}
</View>
)}
</ScrollView>
{showModal && (
<View style={{ position: 'fixed', left: 0, top: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.4)', zIndex: 999 }}>
<View className='absolute bottom-0 left-0 right-0 bg-white rounded-t-2xl p-4'>
<Text className='text-base font-medium block mb-3'> API Key</Text>
<View className='mb-3'>
<Text className='text-xs text-gray-500 block mb-1'></Text>
<Input
className='bg-gray-100 rounded-lg px-3 py-2 text-sm'
placeholder='如:生产环境'
value={form.name}
onInput={(e) => setForm({ ...form, name: e.detail.value })}
/>
</View>
<View className='mb-4'>
<Text className='text-xs text-gray-500 block mb-1'></Text>
<Textarea
className='bg-gray-100 rounded-lg px-3 py-2 text-sm w-full'
placeholder='可选'
value={form.remark}
onInput={(e) => setForm({ ...form, remark: e.detail.value })}
style={{ height: '70px' }}
/>
</View>
<View className='flex gap-3'>
<View className='flex-1 py-2.5 rounded-lg bg-gray-100 text-center' onClick={() => setShowModal(false)}>
<Text className='text-sm text-gray-600'></Text>
</View>
<View className='flex-1 py-2.5 rounded-lg text-center' style={{ background: '#2563eb' }} onClick={submit}>
<Text className='text-sm text-white'>{submitting ? '提交中...' : '创建'}</Text>
</View>
</View>
</View>
</View>
)}
</View>
)
}
export default ApiKeysPage