feat(user): 合并切换开发者菜单并优化首页轮播图显示
- 用户菜单根据 isDeveloper 字段切换显示申请或进入开发者中心 - 已审核用户菜单高亮显示“进入开发者中心” - 去除冗余开发者中心菜单条目,简化结构 - 重新设计开发者中心页面UI,统一门店中心风格 - 顶部展示开发者头像及昵称手机号信息 - 功能卡片展示“我的应用”、“API Key”和“我的工单”,异步加载数量角标 - 底部添加“开发者帮助与支持”入口,替代门店订阅消息卡片 - 未登录或非开发者状态显示相应空态及操作按钮 - 首页轮播图为空时不显示容器,功能入口自动上移,优化空状态体验 - 优化登录状态与权限校验逻辑,防止页面闪烁和无权限展示
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// 应用产品(对齐 PC /developer/apps 与 /admin 应用管理,基址走 websopy-api)
|
||||
import { get, post } from '@/utils/request'
|
||||
import { get, post, put, del } from '@/utils/request'
|
||||
import { WebsopyBaseUrl } from '@/config/app'
|
||||
|
||||
const BASE = '/app/product'
|
||||
@@ -57,6 +57,20 @@ export async function pageAllApps(params: any) {
|
||||
return Promise.reject(new Error(res?.message || '加载失败'))
|
||||
}
|
||||
|
||||
/** 更新应用 */
|
||||
export async function updateAppProduct(data: any) {
|
||||
const res = await put<any>(`${BASE}/update`, data, { baseUrl: WebsopyBaseUrl })
|
||||
if (res?.code === 0) return res.data
|
||||
return Promise.reject(new Error(res?.message || '更新失败'))
|
||||
}
|
||||
|
||||
/** 删除应用 */
|
||||
export async function deleteAppProduct(id: number) {
|
||||
const res = await del<any>(`${BASE}/delete/${id}`, {}, { baseUrl: WebsopyBaseUrl })
|
||||
if (res?.code === 0) return res.data
|
||||
return Promise.reject(new Error(res?.message || '删除失败'))
|
||||
}
|
||||
|
||||
/** 审核通过 */
|
||||
export async function approvePublishReview(id: number) {
|
||||
const res = await post<any>(`${BASE}/approve/${id}`, {}, { baseUrl: WebsopyBaseUrl })
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { View, Text, ScrollView, Input, Textarea } from '@tarojs/components'
|
||||
import { View, Text, ScrollView, Input, Textarea, Image } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import NavBar from '@/components/NavBar'
|
||||
import CellRow from '@/components/common/CellRow'
|
||||
import Loading from '@/components/common/Loading'
|
||||
import { useUser } from '@/hooks/useUser'
|
||||
import { getMyApps, addAppProduct } from '@/api/app/appProduct'
|
||||
import { getPageList, APP_TYPE_NAME } from '@/utils/devcenter'
|
||||
import { getMyApps, addAppProduct, updateAppProduct, deleteAppProduct } from '@/api/app/appProduct'
|
||||
import {
|
||||
getPageList,
|
||||
APP_TYPE_NAME,
|
||||
PUBLISH_STATUS_NAME,
|
||||
STATUS_NAME,
|
||||
publishStatusColor,
|
||||
} from '@/utils/devcenter'
|
||||
|
||||
definePageConfig({ navigationBarTitleText: '我的应用' })
|
||||
|
||||
@@ -18,20 +23,62 @@ const APP_TYPE_OPTIONS = [
|
||||
{ type: 110, name: '平台应用' },
|
||||
]
|
||||
|
||||
/** 应用类型 emoji 映射(无 logo 时用) */
|
||||
const APP_TYPE_EMOJI: Record<number, string> = {
|
||||
10: '🌐',
|
||||
20: '💚',
|
||||
30: '🎵',
|
||||
40: '🔍',
|
||||
50: '💙',
|
||||
60: '🤖',
|
||||
70: '🍎',
|
||||
80: '💻',
|
||||
90: '🖥️',
|
||||
100: '🔌',
|
||||
110: '⚙️',
|
||||
}
|
||||
|
||||
/** 运行状态颜色 */
|
||||
function statusColor(status?: number): string {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return '#16a34a'
|
||||
case 2:
|
||||
case 4:
|
||||
return '#d97706'
|
||||
case 3:
|
||||
case 5:
|
||||
return '#dc2626'
|
||||
default:
|
||||
return '#6b7280'
|
||||
}
|
||||
}
|
||||
|
||||
interface AppForm {
|
||||
productId?: number
|
||||
productName: string
|
||||
productCode: string
|
||||
appType: number
|
||||
description: string
|
||||
}
|
||||
|
||||
const EMPTY_FORM: AppForm = { productName: '', productCode: '', appType: 20, description: '' }
|
||||
|
||||
const MyAppsPage: React.FC = () => {
|
||||
const { isLoggedIn } = useUser()
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [list, setList] = useState<any[]>([])
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const [form, setForm] = useState({ productName: '', productCode: '', appType: 20, description: '' })
|
||||
const [form, setForm] = useState<AppForm>(EMPTY_FORM)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [editing, setEditing] = useState(false)
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const data = await getMyApps({ page: 1, limit: 20 })
|
||||
setList(getPageList(data))
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
setLoading(false)
|
||||
@@ -43,7 +90,20 @@ const MyAppsPage: React.FC = () => {
|
||||
}, [isLoggedIn, load])
|
||||
|
||||
const openCreate = () => {
|
||||
setForm({ productName: '', productCode: '', appType: 20, description: '' })
|
||||
setForm(EMPTY_FORM)
|
||||
setEditing(false)
|
||||
setShowModal(true)
|
||||
}
|
||||
|
||||
const openEdit = (app: any) => {
|
||||
setForm({
|
||||
productId: app.productId,
|
||||
productName: app.productName || '',
|
||||
productCode: app.productCode || '',
|
||||
appType: app.appType || 20,
|
||||
description: app.description || '',
|
||||
})
|
||||
setEditing(true)
|
||||
setShowModal(true)
|
||||
}
|
||||
|
||||
@@ -52,53 +112,199 @@ const MyAppsPage: React.FC = () => {
|
||||
Taro.showToast({ title: '请填写应用名称', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!/^[a-z][a-z0-9]*$/.test(form.productCode)) {
|
||||
if (!editing && !/^[a-z][a-z0-9]*$/.test(form.productCode)) {
|
||||
Taro.showToast({ title: '标识须为小写字母/数字', icon: 'none' })
|
||||
return
|
||||
}
|
||||
setSubmitting(true)
|
||||
try {
|
||||
await addAppProduct(form)
|
||||
Taro.showToast({ title: '创建成功', icon: 'success' })
|
||||
if (editing) {
|
||||
await updateAppProduct(form)
|
||||
Taro.showToast({ title: '更新成功', icon: 'success' })
|
||||
} else {
|
||||
await addAppProduct(form)
|
||||
Taro.showToast({ title: '创建成功', icon: 'success' })
|
||||
}
|
||||
setShowModal(false)
|
||||
load()
|
||||
} catch (e: any) {
|
||||
Taro.showToast({ title: e?.message || '创建失败', icon: 'none' })
|
||||
Taro.showToast({ title: e?.message || '操作失败', icon: 'none' })
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = (app: any) => {
|
||||
Taro.showModal({
|
||||
title: '删除应用',
|
||||
content: `确定删除「${app.productName || '未命名应用'}」吗?此操作不可恢复。`,
|
||||
confirmText: '删除',
|
||||
confirmColor: '#dc2626',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) return
|
||||
Taro.showLoading({ title: '删除中...' })
|
||||
try {
|
||||
await deleteAppProduct(app.productId)
|
||||
Taro.hideLoading()
|
||||
Taro.showToast({ title: '删除成功', icon: 'success' })
|
||||
load()
|
||||
} catch (e: any) {
|
||||
Taro.hideLoading()
|
||||
Taro.showToast({ title: e?.message || '删除失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const copyLink = (url: string, label: string) => {
|
||||
if (!url) {
|
||||
Taro.showToast({ title: `${label}未配置`, icon: 'none' })
|
||||
return
|
||||
}
|
||||
Taro.setClipboardData({
|
||||
data: url,
|
||||
success: () => {
|
||||
Taro.showToast({ title: `${label}链接已复制`, icon: 'none' })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-100'>
|
||||
<NavBar title='我的应用' rightText='创建' onRightClick={openCreate} />
|
||||
<ScrollView scrollY>
|
||||
<ScrollView scrollY className='pb-4'>
|
||||
{loading ? (
|
||||
<Loading />
|
||||
) : list.length === 0 ? (
|
||||
<View className='flex flex-col items-center justify-center py-24'>
|
||||
<Text className='text-gray-400 text-sm'>还没有应用,点击右上角创建</Text>
|
||||
<View className='flex flex-col items-center justify-center py-32'>
|
||||
<Text className='text-5xl mb-4'>📦</Text>
|
||||
<Text className='text-gray-400 text-sm mb-1'>还没有应用</Text>
|
||||
<Text className='text-gray-300 text-xs'>点击右上角「创建」开始</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View className='mt-3'>
|
||||
{list.map((app) => (
|
||||
<CellRow
|
||||
key={app.productId}
|
||||
title={app.productName || '未命名应用'}
|
||||
subtitle={`${APP_TYPE_NAME[app.appType as number] || '未知类型'} · ${app.productCode || ''}`}
|
||||
onClick={() => {
|
||||
if (app.adminUrl) Taro.setClipboardData({ data: app.adminUrl })
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<View className='px-3 pt-3'>
|
||||
{list.map((app) => {
|
||||
const pubColor = publishStatusColor(app.publishStatus)
|
||||
const runColor = statusColor(app.status)
|
||||
const emoji = APP_TYPE_EMOJI[app.appType as number] || '📦'
|
||||
const pubName = PUBLISH_STATUS_NAME[app.publishStatus as string] || '开发中'
|
||||
const runName = STATUS_NAME[app.status as number] || '未开通'
|
||||
return (
|
||||
<View
|
||||
key={app.productId}
|
||||
className='bg-white rounded-xl mb-3 overflow-hidden'
|
||||
style={{ boxShadow: '0 1px 4px rgba(0,0,0,0.06)' }}
|
||||
>
|
||||
{/* 卡片头部:图标 + 名称 + 类型 */}
|
||||
<View className='flex items-start p-4'>
|
||||
{/* Logo / Emoji */}
|
||||
<View
|
||||
className='w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0 mr-3'
|
||||
style={{ background: '#f3f4f6' }}
|
||||
>
|
||||
{app.logo ? (
|
||||
<Image
|
||||
src={app.logo}
|
||||
className='w-12 h-12 rounded-xl'
|
||||
mode='aspectFill'
|
||||
/>
|
||||
) : (
|
||||
<Text className='text-2xl'>{emoji}</Text>
|
||||
)}
|
||||
</View>
|
||||
{/* 名称 + 标识 */}
|
||||
<View className='flex-1 min-w-0'>
|
||||
<Text className='text-base font-medium text-gray-800 block truncate'>
|
||||
{app.productName || '未命名应用'}
|
||||
</Text>
|
||||
<Text className='text-xs text-gray-400 mt-0.5 block'>
|
||||
{APP_TYPE_NAME[app.appType as number] || '未知类型'} · {app.productCode || ''}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 状态标签 */}
|
||||
<View className='flex items-center gap-2 px-4 mb-3'>
|
||||
<Text
|
||||
className='text-xs px-2 py-0.5 rounded-full'
|
||||
style={{ color: pubColor, background: pubColor + '1a' }}
|
||||
>
|
||||
{pubName}
|
||||
</Text>
|
||||
<Text
|
||||
className='text-xs px-2 py-0.5 rounded-full'
|
||||
style={{ color: runColor, background: runColor + '1a' }}
|
||||
>
|
||||
{runName}
|
||||
</Text>
|
||||
{app.createTime && (
|
||||
<Text className='text-xs text-gray-300 ml-auto'>
|
||||
{app.createTime.slice(0, 10)}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 描述 */}
|
||||
{app.description && (
|
||||
<View className='px-4 pb-3'>
|
||||
<Text className='text-xs text-gray-500 leading-relaxed' style={{ display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden' }}>
|
||||
{app.description}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 底部操作区 */}
|
||||
<View className='flex items-center border-t border-gray-50'>
|
||||
<View
|
||||
className='flex-1 flex items-center justify-center py-2.5'
|
||||
onClick={() => copyLink(app.adminUrl, '管理后台')}
|
||||
>
|
||||
<Text className='text-xs text-gray-500'>管理后台</Text>
|
||||
</View>
|
||||
<View className='w-px h-4 bg-gray-100' />
|
||||
<View
|
||||
className='flex-1 flex items-center justify-center py-2.5'
|
||||
onClick={() => copyLink(app.homeUrl, '首页')}
|
||||
>
|
||||
<Text className='text-xs text-gray-500'>首页</Text>
|
||||
</View>
|
||||
<View className='w-px h-4 bg-gray-100' />
|
||||
<View
|
||||
className='flex-1 flex items-center justify-center py-2.5'
|
||||
onClick={() => openEdit(app)}
|
||||
>
|
||||
<Text className='text-xs text-blue-500'>编辑</Text>
|
||||
</View>
|
||||
<View className='w-px h-4 bg-gray-100' />
|
||||
<View
|
||||
className='flex-1 flex items-center justify-center py-2.5'
|
||||
onClick={() => handleDelete(app)}
|
||||
>
|
||||
<Text className='text-xs text-red-500'>删除</Text>
|
||||
</View>
|
||||
</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'>创建应用</Text>
|
||||
<View
|
||||
style={{ position: 'fixed', left: 0, top: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.4)', zIndex: 999 }}
|
||||
onClick={() => setShowModal(false)}
|
||||
>
|
||||
<View
|
||||
className='absolute bottom-0 left-0 right-0 bg-white rounded-t-2xl p-4'
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{ paddingBottom: 'env(safe-area-inset-bottom)' }}
|
||||
>
|
||||
<View className='flex items-center justify-between mb-4'>
|
||||
<Text className='text-base font-medium'>{editing ? '编辑应用' : '创建应用'}</Text>
|
||||
<Text className='text-gray-300 text-lg' onClick={() => setShowModal(false)}>×</Text>
|
||||
</View>
|
||||
|
||||
<View className='mb-3'>
|
||||
<Text className='text-xs text-gray-500 block mb-1'>应用名称</Text>
|
||||
<Input
|
||||
@@ -108,15 +314,21 @@ const MyAppsPage: React.FC = () => {
|
||||
onInput={(e) => setForm({ ...form, productName: e.detail.value })}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View className='mb-3'>
|
||||
<Text className='text-xs text-gray-500 block mb-1'>应用标识</Text>
|
||||
<Text className='text-xs text-gray-500 block mb-1'>
|
||||
应用标识{editing && '(不可修改)'}
|
||||
</Text>
|
||||
<Input
|
||||
className='bg-gray-100 rounded-lg px-3 py-2 text-sm'
|
||||
placeholder='如:weishop'
|
||||
value={form.productCode}
|
||||
disabled={editing}
|
||||
style={editing ? { opacity: 0.5 } : {}}
|
||||
onInput={(e) => setForm({ ...form, productCode: e.detail.value })}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View className='mb-3'>
|
||||
<Text className='text-xs text-gray-500 block mb-1'>应用类型</Text>
|
||||
<View className='flex flex-wrap gap-2'>
|
||||
@@ -132,6 +344,7 @@ const MyAppsPage: React.FC = () => {
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='mb-4'>
|
||||
<Text className='text-xs text-gray-500 block mb-1'>描述</Text>
|
||||
<Textarea
|
||||
@@ -142,12 +355,22 @@ const MyAppsPage: React.FC = () => {
|
||||
style={{ height: '80px' }}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View className='flex gap-3'>
|
||||
<View className='flex-1 py-2.5 rounded-lg bg-gray-100 text-center' onClick={() => setShowModal(false)}>
|
||||
<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
|
||||
className='flex-1 py-2.5 rounded-lg text-center'
|
||||
style={{ background: '#2563eb' }}
|
||||
onClick={submit}
|
||||
>
|
||||
<Text className='text-sm text-white'>
|
||||
{submitting ? '提交中...' : editing ? '保存' : '创建'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
Reference in New Issue
Block a user