- 审核通过时不再新增角色,而是更新用户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请求等) - 添加公共工具方法和复用组件用于开发者中心及平台管理功能 - 统一品牌名称为“威数谱软件”,更新文案及界面展现一致性
176 lines
6.3 KiB
TypeScript
176 lines
6.3 KiB
TypeScript
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
|