Compare commits
20 Commits
85a1558e84
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| efcbaa5b60 | |||
| 0713ad4cf7 | |||
| 226e223d96 | |||
| dc72e530c7 | |||
| 5da7d17c55 | |||
| 9fba545d34 | |||
| babcf36439 | |||
| a703dcad90 | |||
| b673e2ebac | |||
| e986222ca7 | |||
| 2ec3383fd0 | |||
| ab1b32af68 | |||
| 5b5b7c9a90 | |||
| 39d3b59434 | |||
| 2ea16e22cf | |||
| 50c98cdcf7 | |||
| 8d4b55098f | |||
| 12d6bdfb06 | |||
| eeb63efe9f | |||
| 43093b505b |
7
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
7
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="Stylelint" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
</profile>
|
||||
</component>
|
||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/websopy-taro.iml" filepath="$PROJECT_DIR$/.idea/websopy-taro.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
12
.idea/websopy-taro.iml
generated
Normal file
12
.idea/websopy-taro.iml
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
415
.idea/workspace.xml
generated
415
.idea/workspace.xml
generated
@@ -4,12 +4,8 @@
|
||||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="2de94e4f-0587-4c9b-8bfd-e87e0ae95b81" name="更改" comment="feat(address): 新增收货地址与售后申请功能 - 新增地址编辑页面,支持地址智能识别、地图选点、地区选择和默认地址设置 - 实现地址列表页面,支持地址查看、删除、设为默认及选择返回结算页 - 新增售后申请页面,支持退款类型选择、商品选择、原因填写、图片上传和提交审核 - 修复 passport 分包配置,移除不存在分包并补充缺失声明,避免 Taro 编译报错 - 新增地址类型定义,增强前端地址数据结构类型安全 - 优化页面交互体验,完善表单校验及错误提示逻辑 - 统一代码格式与命名规范,保持代码风格一致性">
|
||||
<change beforePath="$PROJECT_DIR$/.workbuddy/memory/2026-07-16.md" beforeDir="false" afterPath="$PROJECT_DIR$/.workbuddy/memory/2026-07-16.md" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/config/app.ts" beforeDir="false" afterPath="$PROJECT_DIR$/config/app.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/config/env.js" beforeDir="false" afterPath="$PROJECT_DIR$/config/env.js" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/passport/pay/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/passport/pay/index.tsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/utils/request.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/utils/request.ts" afterDir="false" />
|
||||
<list default="true" id="2de94e4f-0587-4c9b-8bfd-e87e0ae95b81" name="更改" comment="feat(user): 合并切换开发者菜单并优化首页轮播图显示 - 用户菜单根据 isDeveloper 字段切换显示申请或进入开发者中心 - 已审核用户菜单高亮显示“进入开发者中心” - 去除冗余开发者中心菜单条目,简化结构 - 重新设计开发者中心页面UI,统一门店中心风格 - 顶部展示开发者头像及昵称手机号信息 - 功能卡片展示“我的应用”、“API Key”和“我的工单”,异步加载数量角标 - 底部添加“开发者帮助与支持”入口,替代门店订阅消息卡片 - 未登录或非开发者状态显示相应空态及操作按钮 - 首页轮播图为空时不显示容器,功能入口自动上移,优化空状态体验 - 优化登录状态与权限校验逻辑,防止页面闪烁和无权限展示">
|
||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
@@ -31,28 +27,30 @@
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="PropertiesComponent"><![CDATA[{
|
||||
"keyToString": {
|
||||
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"git-widget-placeholder": "master",
|
||||
"junie.onboarding.icon.badge.shown": "true",
|
||||
"last_opened_file_path": "/Users/gxwebsoft/VUE/websopy-taro/src/passport",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.detected.package.tslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
"node.js.selected.package.tslint": "(autodetect)",
|
||||
"nodejs_package_manager_path": "pnpm",
|
||||
"npm.build:weapp.executor": "Run",
|
||||
"npm.dev:weapp.executor": "Run",
|
||||
"settings.editor.selected.configurable": "preferences.pluginManager",
|
||||
"to.speed.mode.migration.done": "true",
|
||||
"ts.external.directory.path": "/Applications/WebStorm.app/Contents/plugins/javascript-plugin/jsLanguageServicesImpl/external",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
<component name="PropertiesComponent">{
|
||||
"keyToString": {
|
||||
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"git-widget-placeholder": "master",
|
||||
"junie.onboarding.icon.badge.shown": "true",
|
||||
"last_opened_file_path": "/Users/gxwebsoft/VUE/websopy-taro/src/passport",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.detected.package.stylelint": "true",
|
||||
"node.js.detected.package.tslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
"node.js.selected.package.stylelint": "/Users/gxwebsoft/VUE/websopy-taro/node_modules/stylelint",
|
||||
"node.js.selected.package.tslint": "(autodetect)",
|
||||
"nodejs_package_manager_path": "pnpm",
|
||||
"npm.build:weapp.executor": "Run",
|
||||
"npm.dev:weapp.executor": "Run",
|
||||
"settings.editor.selected.configurable": "preferences.pluginManager",
|
||||
"to.speed.mode.migration.done": "true",
|
||||
"ts.external.directory.path": "/Users/gxwebsoft/VUE/websopy-taro/node_modules/typescript/lib",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
}
|
||||
}]]></component>
|
||||
}</component>
|
||||
<component name="RecentsManager">
|
||||
<key name="CopyFile.RECENT_KEYS">
|
||||
<recent name="$PROJECT_DIR$/src/passport" />
|
||||
@@ -117,167 +115,10 @@
|
||||
<workItem from="1782971387128" duration="7154000" />
|
||||
<workItem from="1784093204572" duration="6082000" />
|
||||
<workItem from="1784131049625" duration="2304000" />
|
||||
<workItem from="1784133736973" duration="2917000" />
|
||||
</task>
|
||||
<task id="LOCAL-00006" summary="fix(passport): 修正邀请接受接口的URL路径 - 将邀请接受接口URL从/api/_app/invite/accept修改为/api/_app/developer/invite/accept - 确保请求正确指向开发者邀请接受接口 - 修复因URL错误导致的邀请接受失败问题">
|
||||
<option name="closed" value="true" />
|
||||
<created>1775901973624</created>
|
||||
<option name="number" value="00006" />
|
||||
<option name="presentableId" value="LOCAL-00006" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1775901973624</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00007" summary="fix(passport): 优化登录确认成功后的页面跳转逻辑 - 延长登录确认成功提示的显示时间至2000毫秒 - 跳转到用户页面前先隐藏toast,避免影响跳转体验 - 增加switchTab成功和失败的日志打印 - 失败时提示用户手动返回页面 - 调整跳转延迟时间为1800毫秒,提升交互流畅度">
|
||||
<option name="closed" value="true" />
|
||||
<created>1775902053962</created>
|
||||
<option name="number" value="00007" />
|
||||
<option name="presentableId" value="LOCAL-00007" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1775902053962</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00008" summary="fix(passport): 修正邀请信息接口请求参数与请求头 - 将邀请令牌作为查询参数附加在请求URL中 - 移除请求体中的token字段传递 - 添加请求头TenantId,值为5 - 请求失败时在控制台打印错误信息以便调试 - 保持原有错误处理和加载状态设置逻辑不变">
|
||||
<option name="closed" value="true" />
|
||||
<created>1775904794443</created>
|
||||
<option name="number" value="00008" />
|
||||
<option name="presentableId" value="LOCAL-00008" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1775904794443</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00009" summary="fix(invite): 修复邀请加入请求头TenantId传递问题 - 从配置中引入TenantId常量 - 将请求header中的TenantId字段由硬编码改为动态变量 - 确保GET请求和加密数据请求均正确带上TenantId头信息 - 提升接口调用的一致性和维护性">
|
||||
<option name="closed" value="true" />
|
||||
<created>1775905047844</created>
|
||||
<option name="number" value="00009" />
|
||||
<option name="presentableId" value="LOCAL-00009" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1775905047844</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00010" summary="fix(config): 修正环境配置中的API_BASE_URL地址 - 将开发环境、生产环境和测试环境的API_BASE_URL统一更新为新的域名websopy-api.websoft.top - 更新SimpleQRCodeModal组件中二维码请求地址,使用新的API域名 - 在邀请信息请求函数fetchInviteInfo中添加详细的请求和响应日志 - 增加接口错误日志打印,提高捕获异常时的信息丰富度">
|
||||
<option name="closed" value="true" />
|
||||
<created>1775905427131</created>
|
||||
<option name="number" value="00010" />
|
||||
<option name="presentableId" value="LOCAL-00010" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1775905427131</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00011" summary="fix(invite): 修正邀请接口使用独立API域名 - 将邀请相关接口的请求地址由通用API域名改为独立的INVITE_API_URL - 修改获取邀请信息接口的请求URL - 修改接受邀请接口的请求URL - 确保请求头和参数传递保持一致">
|
||||
<option name="closed" value="true" />
|
||||
<created>1775905743204</created>
|
||||
<option name="number" value="00011" />
|
||||
<option name="presentableId" value="LOCAL-00011" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1775905743204</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00012" summary="refactor(api): 移除未使用的配置接口及相关Hook - 从cmsWebsiteField接口中删除configWebsiteField方法 - 删除useConfig自定义Hook及其引用 - 更新about页面,去除对config对象和useConfig的使用 - 修正invite模块请求URL,统一加上/api前缀">
|
||||
<option name="closed" value="true" />
|
||||
<created>1775908253789</created>
|
||||
<option name="number" value="00012" />
|
||||
<option name="presentableId" value="LOCAL-00012" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1775908253789</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00013" summary="refactor(api): 移除未使用的配置接口及相关Hook - 从cmsWebsiteField接口中删除configWebsiteField方法 - 删除useConfig自定义Hook及其引用 - 更新about页面,去除对config对象和useConfig的使用 - 修正invite模块请求URL,统一加上/api前缀">
|
||||
<option name="closed" value="true" />
|
||||
<created>1775908391880</created>
|
||||
<option name="number" value="00013" />
|
||||
<option name="presentableId" value="LOCAL-00013" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1775908391880</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00014" summary="fix(invite): 修正邀请接口路径以支持小程序专用前缀 - 将请求邀请信息接口路径改为使用 /api/_app 前缀,确保小程序免登录访问 - 修改接受邀请接口路径,统一使用小程序专用 API 域名和前缀 - 更新邀请相关接口注释,明确小程序专用接口前缀含义 - 在专家历史数据中添加高级开发工程师 Will 的条目并更新时间戳">
|
||||
<option name="closed" value="true" />
|
||||
<created>1775909013060</created>
|
||||
<option name="number" value="00014" />
|
||||
<option name="presentableId" value="LOCAL-00014" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1775909013060</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00015" summary="fix(passport): 添加邀请接受接口调试日志 - 在接受邀请请求前打印token和请求URL、参数 - 在请求响应后输出接口响应及返回数据 - 在调用失败时打印错误信息及异常日志 - 保持请求流程和状态管理不变,增强调试能力">
|
||||
<option name="closed" value="true" />
|
||||
<created>1775909542310</created>
|
||||
<option name="number" value="00015" />
|
||||
<option name="presentableId" value="LOCAL-00015" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1775909542310</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00016" summary="fix(passport): 添加邀请接受接口调试日志 - 在接受邀请请求前打印token和请求URL、参数 - 在请求响应后输出接口响应及返回数据 - 在调用失败时打印错误信息及异常日志 - 保持请求流程和状态管理不变,增强调试能力">
|
||||
<option name="closed" value="true" />
|
||||
<created>1775909811705</created>
|
||||
<option name="number" value="00016" />
|
||||
<option name="presentableId" value="LOCAL-00016" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1775909811705</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00017" summary="fix(passport): 优化邀请码接受失败的用户提示和处理 - 针对“用户不存在”、“用户创建失败”及“请先登录”提示,改为弹窗提示需登录 - 在弹窗中增加“去登录”按钮,点击后跳转到登录页面,并携带重定向token参数 - 其他错误仍使用原Toast提示方式展示错误信息 - 保留错误日志打印,便于排查异常情况">
|
||||
<option name="closed" value="true" />
|
||||
<created>1775920899787</created>
|
||||
<option name="number" value="00017" />
|
||||
<option name="presentableId" value="LOCAL-00017" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1775920899787</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00018" summary="feat(passport-invite): 优化新用户扫码邀请加入流程 - 扫码进入邀请页面时保存邀请 token 到本地存储 - 新增登录状态检测,未登录用户点击快速加入时保存邀请信息并跳转登录页 - 登录成功后返回邀请页自动执行加入应用操作,提升用户体验 - 支持已登录用户直接加入应用,简化流程 - 增加清理 pending 邀请数据的方法,防止数据残留 - 监听页面显示事件,处理登录返回后自动操作 - 调整接口请求逻辑,确保携带最新的邀请和授权数据 - 更新项目配置,注册邀请页面路径信息">
|
||||
<option name="closed" value="true" />
|
||||
<created>1775921636640</created>
|
||||
<option name="number" value="00018" />
|
||||
<option name="presentableId" value="LOCAL-00018" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1775921636640</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00019" summary="feat(passport): 重写登录与邀请加入流程,支持微信手机号一键登录 - 完全重写 passport/login.tsx,实现微信手机号一键登录功能 - 支持登录页面携带 redirect 参数,登录后自动跳转回原页面 - 登录成功后自动处理邀请加入应用,支持邀请关系绑定 - 重写 passport/invite/index.tsx,实现扫码邀请加入应用完整流程 - 支持调用 wx.login 获取 code,使用 loginByOpenId 判断注册状态 - 未注册用户自动跳转登录页完成微信手机号登录再加入应用 - 已登录用户直接执行加入应用操作,加入接口支持 Authorization 头 - 解决邀请页面与登录页面间旧授权码混用导致加入失败问题 - 处理后端 401 错误,优先使用微信授权码免登录接口 - 优化用户体验,添加服务协议与隐私政策弹窗及错误提示 - 页面状态管理改进,加入加载、检查登录、错误和跳转反馈">
|
||||
<option name="closed" value="true" />
|
||||
<created>1775962292970</created>
|
||||
<option name="number" value="00019" />
|
||||
<option name="presentableId" value="LOCAL-00019" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1775962292970</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00020" summary="feat(invite): 重构邀请加入流程,实现已登录用户免授权直接加入 - 完全重写邀请页逻辑,区分已登录与未注册用户按钮显示 - 已登录用户显示“确认加入”按钮,点击用 wx.login 获取 code 加入 - 未注册用户显示“微信手机号快速加入”按钮,授权手机号后完成注册登录再加入 - 移除跳转登录页面逻辑,所有流程在邀请页内完成 - 加入接口统一使用 Authorization 头携带 token,确保身份双重验证 - 优化背景、粒子和扫描线效果,提升界面视觉体验 - 增加错误处理提示,加入失败时显示详细原因并可返回首页 - 协议勾选仅在未注册用户显示,确保合规授权流程">
|
||||
<option name="closed" value="true" />
|
||||
<created>1775965322794</created>
|
||||
<option name="number" value="00020" />
|
||||
<option name="presentableId" value="LOCAL-00020" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1775965322794</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00021" summary="feat(passport): 优化已登录用户加入时协议勾选逻辑 - 已登录用户点击「确认加入」时不再强制勾选协议 - 移除 handleConfirmJoin 中对协议勾选的检查 - 未注册用户仍需勾选协议后才能授权手机号 - 确保登录状态异常时有提示信息显示">
|
||||
<option name="closed" value="true" />
|
||||
<created>1775965478226</created>
|
||||
<option name="number" value="00021" />
|
||||
<option name="presentableId" value="LOCAL-00021" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1775965478226</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00022" summary="feat(invite): 支持已登录用户直接加入应用 - 后端accept接口code参数改为可选,支持通过Authorization头识别登录用户 - 已登录用户加入时不再弹手机号授权,前端调用新增的doJoinAppForLoggedInUser方法 - 未注册用户仍通过手机号授权码code加入,调用doJoinAppForNewUser方法 - 拆分前端加入逻辑,新增handleConfirmJoin统一处理已登录加入流程 - 优化按钮交互,已登录状态下按钮变为普通点击,移除协议强制勾选 - 错误处理和状态提示完善,加入成功后自动跳转首页">
|
||||
<option name="closed" value="true" />
|
||||
<created>1775966172496</created>
|
||||
<option name="number" value="00022" />
|
||||
<option name="presentableId" value="LOCAL-00022" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1775966172496</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00023" summary="feat(developer): 完成小程序开发者中心和企业控制台改造 - 设计并实现了开发者中心与企业控制台两大模块 - 按用户角色区分开发者和企业客户,支持多项目类型及成员管理 - 新增项目管理、应用管理、API Key管理及成员邀请等多功能页面 - 实现应用版本发布、消息通知中心、权限审批与开发者申请流程 - 完成CI/CD流水线、运营监控、发票管理、SSO单点登录功能 - 搭建SDK下载中心、工单系统、FAQ系统、数据导入导出等模块 - 优化后端API,支持已登录和未注册用户不同加入应用流程 - 前端按钮统一采用微信手机号授权,完善用户授权体验 - 修复多个页面的JSX语法错误及依赖导入问题,替换部分组件库 - 增加详细的类型定义文件,提升项目类型安全 - 新增超过55个页面及60个API接口,扩展应用功能和服务体系 - 完成全面的样式设计,实现一致的视觉风格和交互体验">
|
||||
<option name="closed" value="true" />
|
||||
<created>1776018406658</created>
|
||||
<option name="number" value="00023" />
|
||||
<option name="presentableId" value="LOCAL-00023" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1776018406658</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00024" summary="feat(api-keys): 优化隐私协议同意逻辑及样式 - 修正api-keys样式导入路径,改为developer路径 - 在开发者申请页面增加服务协议和隐私政策勾选框 - 提交时校验协议是否已勾选,未勾选时提示用户 - 优化申请页面协议区域样式及交互 - 在用户验证页面添加隐私政策同意勾选框 - 提交用户验证表单时检查协议同意状态 - 增加打开服务协议和隐私政策的链接跳转 - 更新项目配置,增加passport/webview路由配置 - 修复createTicket函数中日期格式正则表达式问题">
|
||||
<option name="closed" value="true" />
|
||||
<created>1776019228297</created>
|
||||
<option name="number" value="00024" />
|
||||
<option name="presentableId" value="LOCAL-00024" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1776019228297</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00025" summary="feat(api-keys): 优化隐私协议同意逻辑及样式 - 修正api-keys样式导入路径,改为developer路径 - 在开发者申请页面增加服务协议和隐私政策勾选框 - 提交时校验协议是否已勾选,未勾选时提示用户 - 优化申请页面协议区域样式及交互 - 在用户验证页面添加隐私政策同意勾选框 - 提交用户验证表单时检查协议同意状态 - 增加打开服务协议和隐私政策的链接跳转 - 更新项目配置,增加passport/webview路由配置 - 修复createTicket函数中日期格式正则表达式问题">
|
||||
<option name="closed" value="true" />
|
||||
<created>1776019711439</created>
|
||||
<option name="number" value="00025" />
|
||||
<option name="presentableId" value="LOCAL-00025" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1776019711439</updated>
|
||||
<workItem from="1784133736973" duration="4614000" />
|
||||
<workItem from="1784585070286" duration="3047000" />
|
||||
<workItem from="1784638429525" duration="16147000" />
|
||||
<workItem from="1784798575885" duration="4896000" />
|
||||
</task>
|
||||
<task id="LOCAL-00026" summary="refactor(config): 移除开发者中心和企业控制台的自定义导航样式 - 删除了开发者中心页面配置中的 navigationStyle: 'custom' - 删除了企业控制台页面配置中的 navigationStyle: 'custom' - 简化了页面配置,保持默认导航样式 - 有助于提升页面兼容性和维护性">
|
||||
<option name="closed" value="true" />
|
||||
@@ -511,7 +352,167 @@
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1784135922873</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="55" />
|
||||
<task id="LOCAL-00055" summary="feat(passport): 迁移并修复小程序支付页面访问 websopy API 问题 - 迁移 src/passport/pay 相关代码至当前项目,无需替换文件 - 新增环境变量 WEBSOPY_API_BASE_URL,提供 websopy API 基础地址 - 在 config/app.ts 导出 WebsopyBaseUrl,供代码引用 - 配置 env.d.ts 类型定义,消除 TypeScript 隐式 any 错误 - request 请求方法支持自定义 baseUrl 以调用不同接口地址 - passport/pay 页面调用订阅接口统一传入 WebsopyBaseUrl - 修正 tenantId 类型为 number,调整登录用户类型断言 - 确认编译成功,dist 目录生成对应文件及正确请求地址 - 说明 type-check 依赖库问题不影响正常构建和运行">
|
||||
<option name="closed" value="true" />
|
||||
<created>1784136882898</created>
|
||||
<option name="number" value="00055" />
|
||||
<option name="presentableId" value="LOCAL-00055" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1784136882898</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00056" summary="feat(passport): 优化支付成功后页面跳转逻辑 - 修改支付成功后的自动跳转目标,从已购产品页面改为我的页面 - 使用 switchTab 替代 navigateTo 保证在 tabBar 页顺利跳转 - 移除多余的跳转失败回退逻辑,简化跳转流程 - 调整跳转延迟时间为 1500 毫秒,提升用户体验">
|
||||
<option name="closed" value="true" />
|
||||
<created>1784586922535</created>
|
||||
<option name="number" value="00056" />
|
||||
<option name="presentableId" value="LOCAL-00056" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1784586922535</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00057" summary="feat(user): 改为动态查询 VIP 角色ID,避免写死角色ID - RoleParam 类型新增 tenantId 字段,支持按租户过滤角色 - 新增 getCurrentTenantId 获取当前租户ID,支持登录态和默认租户回退 - 实现 getVipRoleId 函数,按 roleCode 与 tenantId 查询 VIP 角色ID - assignVipRole 中动态获取 VIP 角色ID,避免硬编码 - 添加用户角色时带上 tenantId,确保多租户角色绑定正确 - 移除写死的 VIP_ROLE_ID,改用动态查询结果,提高跨租户兼容性 - 补充引入 listRoles 和 TenantId,完善相关依赖引用">
|
||||
<option name="closed" value="true" />
|
||||
<created>1784639229566</created>
|
||||
<option name="number" value="00057" />
|
||||
<option name="presentableId" value="LOCAL-00057" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1784639229566</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00058" summary="feat(user): 动态查询VIP角色ID,避免硬编码绑定问题 - RoleParam新增tenantId字段,支持按租户过滤角色 - 新增getCurrentTenantId函数,兼容登录态及默认租户回退 - 实现getVipRoleId函数,按roleCode与tenantId查询VIP角色ID - assignVipRole动态获取VIP角色ID,移除写死的角色ID - 添加用户角色时传入tenantId,确保多租户环境中角色绑定正确 - 修复后端UserRoleController.save接口bug,避免userId被无条件覆盖 - 用户角色绑定时正确使用申请人userId,解决VIP审核角色误绑定问题">
|
||||
<option name="closed" value="true" />
|
||||
<created>1784653464775</created>
|
||||
<option name="number" value="00058" />
|
||||
<option name="presentableId" value="LOCAL-00058" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1784653464775</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00059" summary="feat(user): 统一VIP概念为开发者并优化开发者登录及申请 - 将VIP相关文案、图标改为开发者,涉及用户中心、门店中心、审核页等 - 优化登录弹窗UI,改为红色系,提升视觉统一性和交互体验 - 关于我们页面品牌和公司信息内容更新,展示最新企业介绍和业务范围 - VIP升级页新增“所在城市”选择,使用微信原生地区选择器,完善地址信息 - 用户申请表单中新增城市字段及校验,提交时携带城市信息 - 优化审核页面文案,统称开发者身份,明确申请审核内容 - 修复UserRoleController.save接口bug,避免userId被无条件覆盖导致角色绑错 - 动态查询VIP角色ID,避免硬编码,支持多租户环境正确绑定角色">
|
||||
<option name="closed" value="true" />
|
||||
<created>1784697551142</created>
|
||||
<option name="number" value="00059" />
|
||||
<option name="presentableId" value="LOCAL-00059" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1784697551142</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00060" summary="refactor(user): 删除门店信息输入框 - 移除用户资料页面中的门店名称输入框 - 移除用户资料页面中的门店地址输入框 - 简化用户页面界面布局,提高界面整洁度">
|
||||
<option name="closed" value="true" />
|
||||
<created>1784697730666</created>
|
||||
<option name="number" value="00060" />
|
||||
<option name="presentableId" value="LOCAL-00060" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1784697730666</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00061" summary="refactor(user): 统一品牌名称为“威数谱软件” - 将项目中所有“威硕谱软件”名称修改为“威数谱软件” - 更新协议页面、欢迎消息、礼品卡等多处文本内容 - 修改小程序配置文件中的项目名称和显示标题 - 优化开发者申请页部分文案,明确开发者身份描述 - 保证所有界面及配置中的品牌名称统一一致">
|
||||
<option name="closed" value="true" />
|
||||
<created>1784699171525</created>
|
||||
<option name="number" value="00061" />
|
||||
<option name="presentableId" value="LOCAL-00061" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1784699171525</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00062" summary="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请求等) - 添加公共工具方法和复用组件用于开发者中心及平台管理功能 - 统一品牌名称为“威数谱软件”,更新文案及界面展现一致性">
|
||||
<option name="closed" value="true" />
|
||||
<created>1784703425108</created>
|
||||
<option name="number" value="00062" />
|
||||
<option name="presentableId" value="LOCAL-00062" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1784703425108</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00063" summary="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请求等) - 添加公共工具方法和复用组件用于开发者中心及平台管理功能 - 统一品牌名称为“威数谱软件”,更新文案及界面展现一致性">
|
||||
<option name="closed" value="true" />
|
||||
<created>1784704220175</created>
|
||||
<option name="number" value="00063" />
|
||||
<option name="presentableId" value="LOCAL-00063" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1784704220175</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00064" summary="feat(weapp): 新增开发者中心与平台管理入口及相关功能 - 小程序端新增开发者中心和平台管理页面模块,完善功能入口 - 新增开发者中心页面(概览、我的应用、API Key和工单) - 新增平台管理页面(管理首页、应用审核、Git权限审核、域名绑定审核和工单处理) - 复刻PC端/api/app/*相关API接口,基地址使用WEBSOPY_API_BASE_URL - 增加公共组件StatCard、EntryGrid、CellRow以支持新页面UI - 修改首页增加开发者中心和平台管理角色入口,仅对对应用户显示 - 用户页面菜单新增开发者中心和平台管理入口条件展示 - 约定开发者身份使用sys_user.is_developer布尔字段,不再依赖角色表 - 更新登录态业务,菜单是否显示基于登录接口返回的isDeveloper/isAdmin字段 - 确保相关页面已在app.config.ts注册并添加身份认证需求 - 调整运行配置为npm build:weapp,确保编译无误">
|
||||
<option name="closed" value="true" />
|
||||
<created>1784707342939</created>
|
||||
<option name="number" value="00064" />
|
||||
<option name="presentableId" value="LOCAL-00064" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1784707342940</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00065" summary="feat(weapp): 新增开发者中心与平台管理入口及功能 - 小程序端新增开发者中心和平台管理页面模块,完善功能入口 - 新增开发者中心页面(概览、我的应用、API Key和工单) - 新增平台管理页面(管理首页、应用审核、Git权限审核、域名绑定审核和工单处理) - 复刻PC端/api/app/*相关API接口,基地址使用WEBSOPY_API_BASE_URL - 增加公共组件StatCard、EntryGrid、CellRow以支持新页面UI - 修改首页增加开发者中心和平台管理角色入口,仅对对应用户显示 - 用户页面菜单新增开发者中心和平台管理入口条件展示 - 约定开发者身份使用sys_user.is_developer布尔字段,不再依赖角色表 - 更新登录态业务,菜单是否显示基于登录接口返回的isDeveloper/isAdmin字段 - vip-review审核通过流程调整为只调用setUserDeveloper,后台替换角色为developer并置isDeveloper=1">
|
||||
<option name="closed" value="true" />
|
||||
<created>1784709736683</created>
|
||||
<option name="number" value="00065" />
|
||||
<option name="presentableId" value="LOCAL-00065" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1784709736683</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00066" summary="fix(weapp): 修复支付页nutui组件导致内容空白问题 - 替换支付页中nutui的Button、Cell、Price、Divider组件为Taro原生组件或项目自定义组件 - 修正商品名称、价格类型和支付金额显示,使用flex布局及Text组件自绘内容 - 调整按钮样式,统一为带蓝色背景和圆角的自定义样式 - 移除nutui Price组件中的thousands属性,使用项目自定义Price组件 - 解决微信小程序中nutui组件因div渲染缺失导致UI元素不显示的问题 - 保持支付流程及状态逻辑无改动,仅优化展示兼容性和稳定性">
|
||||
<option name="closed" value="true" />
|
||||
<created>1784798603228</created>
|
||||
<option name="number" value="00066" />
|
||||
<option name="presentableId" value="LOCAL-00066" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1784798603228</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00067" summary="fix(weapp): 修复支付页nutui组件导致内容空白问题 - 替换支付页中nutui的Button、Cell、Price、Divider组件为Taro原生组件或项目自定义组件 - 修正商品名称、价格类型和支付金额显示,使用flex布局及Text组件自绘内容 - 调整按钮样式,统一为带蓝色背景和圆角的自定义样式 - 移除nutui Price组件中的thousands属性,使用项目自定义Price组件 - 解决微信小程序中nutui组件因div渲染缺失导致UI元素不显示的问题 - 保持支付流程及状态逻辑无改动,仅优化展示兼容性和稳定性">
|
||||
<option name="closed" value="true" />
|
||||
<created>1784799951929</created>
|
||||
<option name="number" value="00067" />
|
||||
<option name="presentableId" value="LOCAL-00067" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1784799951929</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00068" summary="fix(weapp): 修复支付页nutui组件导致内容空白问题 - 替换支付页中nutui的Button、Cell、Price、Divider组件为Taro原生组件或项目自定义组件 - 修正商品名称、价格类型和支付金额显示,使用flex布局及Text组件自绘内容 - 调整按钮样式,统一为带蓝色背景和圆角的自定义样式 - 移除nutui Price组件中的thousands属性,使用项目自定义Price组件 - 解决微信小程序中nutui组件因div渲染缺失导致UI元素不显示的问题 - 保持支付流程及状态逻辑无改动,仅优化展示兼容性和稳定性">
|
||||
<option name="closed" value="true" />
|
||||
<created>1784800111045</created>
|
||||
<option name="number" value="00068" />
|
||||
<option name="presentableId" value="LOCAL-00068" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1784800111045</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00069" summary="feat(user): 补充开发者审核通过时添加店员记录功能 - 引入 addShopStoreUser 接口用于新增门店店员记录 - 新增 clerkStoreId 和 clerkTenantId 状态,保存当前审核店员的门店和租户信息 - 在审核通过后调用 addShopStoreUser,添加申请人为当前门店店员 - 保证只有当 clerkStoreId 存在时才执行新增操作 - 更新工作记录文档,补充审核通过流程中新添加的店员记录步骤 - 保持原有升级开发者逻辑和接口调用不变">
|
||||
<option name="closed" value="true" />
|
||||
<created>1784800763746</created>
|
||||
<option name="number" value="00069" />
|
||||
<option name="presentableId" value="LOCAL-00069" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1784800763746</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00070" summary="feat(user): 补充开发者审核通过时添加店员记录功能 - 引入 addShopStoreUser 接口用于新增门店店员记录 - 新增 clerkStoreId 和 clerkTenantId 状态,保存当前审核店员的门店和租户信息 - 在审核通过后调用 addShopStoreUser,添加申请人为当前门店店员 - 保证只有当 clerkStoreId 存在时才执行新增操作 - 调整平台管理页 UI,使其与门店中心保持一致风格 - 门店中心及平台管理入口权限调整,仅超级管理员可访问 - 用户菜单中平台管理和门店中心入口现基于 isSuperAdmin 字段控制 - 优化登录及权限校验流程,避免权限不足时页面闪烁显示 - 更新工作记录文档,补充审核通过流程中新添加的店员记录步骤">
|
||||
<option name="closed" value="true" />
|
||||
<created>1784802336157</created>
|
||||
<option name="number" value="00070" />
|
||||
<option name="presentableId" value="LOCAL-00070" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1784802336157</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00071" summary="feat(user): 合并并切换“申请开发者”菜单项状态 - 在用户菜单中根据 isDeveloper 字段切换显示“申请成为开发者”或“进入开发者中心” - 已通过审核用户直接显示“进入开发者中心”且高亮显示 - 去除冗余的独立“开发者中心”菜单条目,简化菜单结构 - 调整平台管理页和门店中心入口权限为仅超级管理员可访问 - 优化登录与权限校验流程,防止权限不足时页面闪烁或无访问权限空白 - 更新相关文档,完善审核通过后店员记录添加流程">
|
||||
<option name="closed" value="true" />
|
||||
<created>1784802434477</created>
|
||||
<option name="number" value="00071" />
|
||||
<option name="presentableId" value="LOCAL-00071" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1784802434477</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00072" summary="fix(auth): 统一超级管理员标识为布尔值true - 将所有页面和菜单中超级管理员校验由数字1改为布尔true - 平台管理、门店中心菜单项仅在isSuperAdmin===true时显示 - 门店中心页面移除店员门槛,改为超级管理员访问限制 - 非超级管理员访问门店中心弹窗提示并返回 - 首页工作台入口及相关访问权限判定切换为布尔判断 - 优化首屏加载状态,避免权限校验闪烁和误判">
|
||||
<option name="closed" value="true" />
|
||||
<created>1784802589434</created>
|
||||
<option name="number" value="00072" />
|
||||
<option name="presentableId" value="LOCAL-00072" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1784802589434</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00073" summary="feat(user): 合并切换开发者菜单项状态并更新开发者中心UI - 用户菜单中根据 isDeveloper 字段条件展示“申请成为开发者”或“进入开发者中心” - 已审核通过用户菜单显示“进入开发者中心”,并设置高亮样式 - 去除冗余的独立“开发者中心”菜单条目,简化菜单结构 - 重新设计开发者中心页面UI,统一为门店中心风格 - 顶部展示开发者头像及昵称手机号信息 - 功能卡片展示“我的应用”、“API Key”和“我的工单”,异步加载数量角标 - 底部添加“开发者帮助与支持”入口,替代门店订阅消息卡片 - 未登录或非开发者状态显示相应空态提示及操作按钮 - 优化登录状态与权限校验逻辑,防止页面闪烁和无权限内容显示">
|
||||
<option name="closed" value="true" />
|
||||
<created>1784802954848</created>
|
||||
<option name="number" value="00073" />
|
||||
<option name="presentableId" value="LOCAL-00073" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1784802954848</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00074" summary="feat(user): 合并切换开发者菜单并优化首页轮播图显示 - 用户菜单根据 isDeveloper 字段切换显示申请或进入开发者中心 - 已审核用户菜单高亮显示“进入开发者中心” - 去除冗余开发者中心菜单条目,简化结构 - 重新设计开发者中心页面UI,统一门店中心风格 - 顶部展示开发者头像及昵称手机号信息 - 功能卡片展示“我的应用”、“API Key”和“我的工单”,异步加载数量角标 - 底部添加“开发者帮助与支持”入口,替代门店订阅消息卡片 - 未登录或非开发者状态显示相应空态及操作按钮 - 首页轮播图为空时不显示容器,功能入口自动上移,优化空状态体验 - 优化登录状态与权限校验逻辑,防止页面闪烁和无权限展示">
|
||||
<option name="closed" value="true" />
|
||||
<created>1784803190504</created>
|
||||
<option name="number" value="00074" />
|
||||
<option name="presentableId" value="LOCAL-00074" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1784803190504</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="75" />
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
@@ -531,23 +532,6 @@
|
||||
<component name="VcsManagerConfiguration">
|
||||
<option name="CHECK_CODE_SMELLS_BEFORE_PROJECT_COMMIT" value="false" />
|
||||
<option name="CHECK_NEW_TODO" value="false" />
|
||||
<MESSAGE value="fix(invite): 修正邀请接口路径以支持小程序专用前缀 - 将请求邀请信息接口路径改为使用 /api/_app 前缀,确保小程序免登录访问 - 修改接受邀请接口路径,统一使用小程序专用 API 域名和前缀 - 更新邀请相关接口注释,明确小程序专用接口前缀含义 - 在专家历史数据中添加高级开发工程师 Will 的条目并更新时间戳" />
|
||||
<MESSAGE value="fix(passport): 添加邀请接受接口调试日志 - 在接受邀请请求前打印token和请求URL、参数 - 在请求响应后输出接口响应及返回数据 - 在调用失败时打印错误信息及异常日志 - 保持请求流程和状态管理不变,增强调试能力" />
|
||||
<MESSAGE value="fix(passport): 优化邀请码接受失败的用户提示和处理 - 针对“用户不存在”、“用户创建失败”及“请先登录”提示,改为弹窗提示需登录 - 在弹窗中增加“去登录”按钮,点击后跳转到登录页面,并携带重定向token参数 - 其他错误仍使用原Toast提示方式展示错误信息 - 保留错误日志打印,便于排查异常情况" />
|
||||
<MESSAGE value="feat(passport-invite): 优化新用户扫码邀请加入流程 - 扫码进入邀请页面时保存邀请 token 到本地存储 - 新增登录状态检测,未登录用户点击快速加入时保存邀请信息并跳转登录页 - 登录成功后返回邀请页自动执行加入应用操作,提升用户体验 - 支持已登录用户直接加入应用,简化流程 - 增加清理 pending 邀请数据的方法,防止数据残留 - 监听页面显示事件,处理登录返回后自动操作 - 调整接口请求逻辑,确保携带最新的邀请和授权数据 - 更新项目配置,注册邀请页面路径信息" />
|
||||
<MESSAGE value="feat(passport): 重写登录与邀请加入流程,支持微信手机号一键登录 - 完全重写 passport/login.tsx,实现微信手机号一键登录功能 - 支持登录页面携带 redirect 参数,登录后自动跳转回原页面 - 登录成功后自动处理邀请加入应用,支持邀请关系绑定 - 重写 passport/invite/index.tsx,实现扫码邀请加入应用完整流程 - 支持调用 wx.login 获取 code,使用 loginByOpenId 判断注册状态 - 未注册用户自动跳转登录页完成微信手机号登录再加入应用 - 已登录用户直接执行加入应用操作,加入接口支持 Authorization 头 -" />
|
||||
<MESSAGE value="feat(invite): 重构邀请加入流程,实现已登录用户免授权直接加入 - 完全重写邀请页逻辑,区分已登录与未注册用户按钮显示 - 已登录用户显示“确认加入”按钮,点击用 wx.login 获取 code 加入 - 未注册用户显示“微信手机号快速加入”按钮,授权手机号后完成注册登录再加入 - 移除跳转登录页面逻辑,所有流程在邀请页内完成 - 加入接口统一使用 Authorization 头携带 token,确保身份双重验证 - 优化背景、粒子和扫描线效果,提升界面视觉体验 - 增加错误处理提示,加入失败时显示详细原因并可返回首页 - 协议勾选仅在未注册用户显示,确保合规授权流程" />
|
||||
<MESSAGE value="feat(passport): 优化已登录用户加入时协议勾选逻辑 - 已登录用户点击「确认加入」时不再强制勾选协议 - 移除 handleConfirmJoin 中对协议勾选的检查 - 未注册用户仍需勾选协议后才能授权手机号 - 确保登录状态异常时有提示信息显示" />
|
||||
<MESSAGE value="feat(invite): 支持已登录用户直接加入应用 - 后端accept接口code参数改为可选,支持通过Authorization头识别登录用户 - 已登录用户加入时不再弹手机号授权,前端调用新增的doJoinAppForLoggedInUser方法 - 未注册用户仍通过手机号授权码code加入,调用doJoinAppForNewUser方法 - 拆分前端加入逻辑,新增handleConfirmJoin统一处理已登录加入流程 - 优化按钮交互,已登录状态下按钮变为普通点击,移除协议强制勾选 - 错误处理和状态提示完善,加入成功后自动跳转首页" />
|
||||
<MESSAGE value="feat(developer): 完成小程序开发者中心和企业控制台改造 - 设计并实现了开发者中心与企业控制台两大模块 - 按用户角色区分开发者和企业客户,支持多项目类型及成员管理 - 新增项目管理、应用管理、API Key管理及成员邀请等多功能页面 - 实现应用版本发布、消息通知中心、权限审批与开发者申请流程 - 完成CI/CD流水线、运营监控、发票管理、SSO单点登录功能 - 搭建SDK下载中心、工单系统、FAQ系统、数据导入导出等模块 - 优化后端API,支持已登录和未注册用户不同加入应用流程 - 前端按钮统一采用微信手机号授权,完善用户授权体验 - 修复多个页面的JSX语法错误及依赖导入问题,替换部分组件库 - 增加详细的类型定义文件,提升项目类型安全 - 新增超过55个页面及60个API接口,扩展应用功能和服务体系 - 完成全面的样式设计,实现一致的视觉风格和交互体验" />
|
||||
<MESSAGE value="feat(api-keys): 优化隐私协议同意逻辑及样式 - 修正api-keys样式导入路径,改为developer路径 - 在开发者申请页面增加服务协议和隐私政策勾选框 - 提交时校验协议是否已勾选,未勾选时提示用户 - 优化申请页面协议区域样式及交互 - 在用户验证页面添加隐私政策同意勾选框 - 提交用户验证表单时检查协议同意状态 - 增加打开服务协议和隐私政策的链接跳转 - 更新项目配置,增加passport/webview路由配置 - 修复createTicket函数中日期格式正则表达式问题" />
|
||||
<MESSAGE value="refactor(config): 移除开发者中心和企业控制台的自定义导航样式 - 删除了开发者中心页面配置中的 navigationStyle: 'custom' - 删除了企业控制台页面配置中的 navigationStyle: 'custom' - 简化了页面配置,保持默认导航样式 - 有助于提升页面兼容性和维护性" />
|
||||
<MESSAGE value="feat(userVerify): 添加阿里云身份证二要素核验功能 - 在接口层新增 verifyIdCard 接口,实现姓名与身份证号核验 - userVerify 页面提交时增加身份核验逻辑,核验失败阻止提交 - 调用核验接口时显示加载状态,并根据结果提示用户 - 后端依赖 aliyun cloudauth,提供实人认证相关服务接口 - 完成基本的错误捕获与用户提示,提升实名认证流程安全性" />
|
||||
<MESSAGE value="refactor(api): 规范前端API接口地址并统一路径前缀 - 修正开发者、小程序和企业相关API的基础URL,避免重复添加/api前缀 - 统一调整developer、enterprise、invite等模块接口路径,保持与后端Controller一致 - 新增权限申请及消息通知相关API,并完善相关函数实现 - 规范请求参数传递,移除多余包装,改用直接传递对象方式 - 新增API Key重置、版本发布/回滚等接口支持 feat(invite): 重构邀请登录流程支持未注册快速加入 - 完整重写invite页,分离已注册用户和未注册用户的按钮显示和交互 - 实现未注册用户微信手机号授权后自动注册登录及自动加入应用 - 已注册用户直接确认加入应用,无需手机号授权弹窗 - 统一使用getPhoneNumber按钮处理两种状态并修复“授权码不能为空”报错 - 前端配合后端改造,支持已登录用户可直接通过Authorization头加入应用 fix(developer): 修复开发者中心应用加载问题 - 修复只查询创建的应用导致邀请加入的应用不显示的问题 - 同时请求创建应用和参与应用接口,合并并去重应用列表 - 新增pageJoinedApp接口调用及合并逻辑,提高应用列表完整性和体验" />
|
||||
<MESSAGE value="feat(app): 添加应用入口文件和配置文件 - 新增 index.tsx 作为应用的主入口文件 - 新增 index.config.ts 用于应用的配置管理" />
|
||||
<MESSAGE value="fix(tailwind): 更新配置以兼容微信小程序 - 启用 autoprefixer 并新增 postcss-nested 插件支持嵌套语法 - 扩展主题颜色,添加 primary 色系 - 更新 content 路径包含 public/index.html - 禁用 preflight 以移除默认样式重置 - 移除多数 corePlugins 禁用项,保留 preflight 禁用,简化兼容性配置" />
|
||||
<MESSAGE value="feat(postcss): 添加插件移除微信小程序不兼容Tailwind规则 - 实现 postcss-remove-wxss-incompatible 插件 - 移除反斜杠转义的重要性修饰符类选择器(.\!xxx) - 删除依赖 :not 和兄弟选择器的 .space-[x|y]-* 规则 - 删除依赖 :not 和兄弟选择器的 .divide-[x|y]-* 规则 - 解决微信小程序 WXSS 不支持的 CSS 特性问题" />
|
||||
<MESSAGE value="fix(pay): 优化订阅号获取逻辑并调整支付组件样式 - 优先从小程序码 scene 参数读取 subscriptionNo,提升参数兼容性 - 对 scene 参数进行解码,避免编码引起的问题 - 移除支付组件价格显示处的固定颜色设置,增强样式灵活性" />
|
||||
<MESSAGE value="fix(utils): 优化小程序端复制文本功能体验 - 小程序端直接调用原生 wx.setClipboardData,避免 Taro Promise 封装问题 - 避免微信小程序重复显示复制成功提示和误报复制失败 - 失败时仅在小程序端输出错误日志,非小程序端显示提示 - 修改首页外链跳转逻辑,支持相对路径跳转,小程序内外链复制提示提升用户体验 - 更新项目内剪切板使用规范文档,说明复制行为调整及符合相应规范要求" />
|
||||
<MESSAGE value="fix(index): 优化外链提示及删除复制咨询模板功能 - 替换小程序内外链打开方式提示为弹窗提醒用户手动复制 - 删除复制咨询模板功能及相关按钮 - 修改联系我们文案,突出直接电话沟通或需求反馈 - 移除无用导入的复制文本工具函数copyText" />
|
||||
<MESSAGE value="feat(utils): 优化请求函数URL拼接逻辑 - 在request函数中将请求拦截改为先拼接完整URL - 支持相对路径自动补全baseUrl - 删除原有请求拦截中url未处理的问题" />
|
||||
@@ -556,7 +540,24 @@
|
||||
<MESSAGE value="feat(pay): 支付成功后自动跳转到已购产品页面 - 支付成功后显示成功提示 - 延迟1.5秒自动跳转到已购产品页面 - 处理跳转失败时回退到用户首页 - 优化用户支付完成后的使用体验" />
|
||||
<MESSAGE value="feat(pay-bak): 新增小程序支付页面及相关配置 - 新增确认支付页面配置,设置导航栏标题和样式 - 实现小程序支付页完整组件逻辑,包括加载订单、发起JSAPI支付等 - 支持通过 scene 或 subscriptionNo 获取订单参数 - 完善支付流程接口请求及错误处理,支持自动登录和支付确认 - 增加支付成功、支付中、支付失败、取消等状态页面展示 - 支持支付成功后跳转已购产品页或用户页 - 提供取消支付返回逻辑和界面展示 - 细化支付金额显示和提示用户支付后可使用应用功能" />
|
||||
<MESSAGE value="feat(address): 新增收货地址与售后申请功能 - 新增地址编辑页面,支持地址智能识别、地图选点、地区选择和默认地址设置 - 实现地址列表页面,支持地址查看、删除、设为默认及选择返回结算页 - 新增售后申请页面,支持退款类型选择、商品选择、原因填写、图片上传和提交审核 - 修复 passport 分包配置,移除不存在分包并补充缺失声明,避免 Taro 编译报错 - 新增地址类型定义,增强前端地址数据结构类型安全 - 优化页面交互体验,完善表单校验及错误提示逻辑 - 统一代码格式与命名规范,保持代码风格一致性" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="feat(address): 新增收货地址与售后申请功能 - 新增地址编辑页面,支持地址智能识别、地图选点、地区选择和默认地址设置 - 实现地址列表页面,支持地址查看、删除、设为默认及选择返回结算页 - 新增售后申请页面,支持退款类型选择、商品选择、原因填写、图片上传和提交审核 - 修复 passport 分包配置,移除不存在分包并补充缺失声明,避免 Taro 编译报错 - 新增地址类型定义,增强前端地址数据结构类型安全 - 优化页面交互体验,完善表单校验及错误提示逻辑 - 统一代码格式与命名规范,保持代码风格一致性" />
|
||||
<MESSAGE value="feat(passport): 迁移并修复小程序支付页面访问 websopy API 问题 - 迁移 src/passport/pay 相关代码至当前项目,无需替换文件 - 新增环境变量 WEBSOPY_API_BASE_URL,提供 websopy API 基础地址 - 在 config/app.ts 导出 WebsopyBaseUrl,供代码引用 - 配置 env.d.ts 类型定义,消除 TypeScript 隐式 any 错误 - request 请求方法支持自定义 baseUrl 以调用不同接口地址 - passport/pay 页面调用订阅接口统一传入 WebsopyBaseUrl - 修正 tenantId 类型为 number,调整登录用户类型断言 - 确认编译成功,dist 目录生成对应文件及正确请求地址 - 说明 type-check 依赖库问题不影响正常构建和运行" />
|
||||
<MESSAGE value="feat(passport): 优化支付成功后页面跳转逻辑 - 修改支付成功后的自动跳转目标,从已购产品页面改为我的页面 - 使用 switchTab 替代 navigateTo 保证在 tabBar 页顺利跳转 - 移除多余的跳转失败回退逻辑,简化跳转流程 - 调整跳转延迟时间为 1500 毫秒,提升用户体验" />
|
||||
<MESSAGE value="feat(user): 改为动态查询 VIP 角色ID,避免写死角色ID - RoleParam 类型新增 tenantId 字段,支持按租户过滤角色 - 新增 getCurrentTenantId 获取当前租户ID,支持登录态和默认租户回退 - 实现 getVipRoleId 函数,按 roleCode 与 tenantId 查询 VIP 角色ID - assignVipRole 中动态获取 VIP 角色ID,避免硬编码 - 添加用户角色时带上 tenantId,确保多租户角色绑定正确 - 移除写" />
|
||||
<MESSAGE value="feat(user): 动态查询VIP角色ID,避免硬编码绑定问题 - RoleParam新增tenantId字段,支持按租户过滤角色 - 新增getCurrentTenantId函数,兼容登录态及默认租户回退 - 实现getVipRoleId函数,按roleCode与tenantId查询VIP角色ID - assignVipRole动态获取VIP角色ID,移除写死的角色ID - 添加用户角色时传" />
|
||||
<MESSAGE value="feat(user): 统一VIP概念为开发者并优化开发者登录及申请 - 将VIP相关文案、图标改为开发者,涉及用户中心、门店中心、审核页等 - 优化登录弹窗UI,改为红色系,提升视觉统一性和交互体验 - 关于我们页面品牌和公司信息内容更新,展示最新企业介绍和业务范围 - VIP升级页新增“所在城市”选择,使用微信原生地区选择器,完善地址信息 - 用户申请表单中新增城市字段及校验,提交时携带城市信息 - 优化审核页面文案,统称开发者身份,明确申请审核内容 - 修复UserRoleController.save接口bug,避免userId被无条件覆盖导致角色绑错 - 动态查询VIP角色ID,避免硬编码,支持多租户环境正确绑定角色" />
|
||||
<MESSAGE value="refactor(user): 删除门店信息输入框 - 移除用户资料页面中的门店名称输入框 - 移除用户资料页面中的门店地址输入框 - 简化用户页面界面布局,提高界面整洁度" />
|
||||
<MESSAGE value="refactor(user): 统一品牌名称为“威数谱软件” - 将项目中所有“威硕谱软件”名称修改为“威数谱软件” - 更新协议页面、欢迎消息、礼品卡等多处文本内容 - 修改小程序配置文件中的项目名称和显示标题 - 优化开发者申请页部分文案,明确开发者身份描述 - 保证所有界面及配置中的品牌名称统一一致" />
|
||||
<MESSAGE value="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请求等) - 添加公共工具方法和复用组件用于开发者中心及平台管理功能 - 统一品牌名称为“威数谱软件”,更新文案及界面展现一致性" />
|
||||
<MESSAGE value="feat(weapp): 新增开发者中心与平台管理入口及相关功能 - 小程序端新增开发者中心和平台管理页面模块,完善功能入口 - 新增开发者中心页面(概览、我的应用、API Key和工单) - 新增平台管理页面(管理首页、应用审核、Git权限审核、域名绑定审核和工单处理) - 复刻PC端/api/app/*相关API接口,基地址使用WEBSOPY_API_BASE_URL - 增加公共组件StatCard、EntryGrid、CellRow以支持新页面UI - 修改首页增加开发者中心和平台管理角色入口,仅对对应用户显示 - 用户页面菜单新增开发者中心和平台管理入口条件展示 - 约定开发者身份使用sys_user.is_developer布尔字段,不再依赖角色表 - 更新登录态业务,菜单是否显示基于登录接口返回" />
|
||||
<MESSAGE value="feat(weapp): 新增开发者中心与平台管理入口及功能 - 小程序端新增开发者中心和平台管理页面模块,完善功能入口 - 新增开发者中心页面(概览、我的应用、API Key和工单) - 新增平台管理页面(管理首页、应用审核、Git权限审核、域名绑定审核和工单处理) - 复刻PC端/api/app/*相关API接口,基地址使用WEBSOPY_API_BASE_URL - 增加公共组件StatCard、EntryGrid、CellRow以支持新页面UI - 修改首页增加开发者中心和平台管理角色入口,仅对对应用户显示 - 用户页面菜单新增开发者中心和平台管理入口条件展示 - 约定开发者身份使用sys_user.is_developer布尔字段,不再依赖角色表 - 更新登录态业务,菜单是否显示基于登录接口返回的isDeveloper/isAdmin字段 - vip-review审核通过流程调整为只调用setUserDeveloper,后台替换角色为developer并置isDeveloper=1" />
|
||||
<MESSAGE value="fix(weapp): 修复支付页nutui组件导致内容空白问题 - 替换支付页中nutui的Button、Cell、Price、Divider组件为Taro原生组件或项目自定义组件 - 修正商品名称、价格类型和支付金额显示,使用flex布局及Text组件自绘内容 - 调整按钮样式,统一为带蓝色背景和圆角的自定义样式 - 移除nutui Price组件中的thousands属性,使用项目自定义Price组件 - 解决微信小程序中nutui组件因div渲染缺失导致UI元素不显示的问题 - 保持支付流程及状态逻辑无改动,仅优化展示兼容性和稳定性" />
|
||||
<MESSAGE value="feat(user): 补充开发者审核通过时添加店员记录功能 - 引入 addShopStoreUser 接口用于新增门店店员记录 - 新增 clerkStoreId 和 clerkTenantId 状态,保存当前审核店员的门店和租户信息 - 在审核通过后调用 addShopStoreUser,添加申请人为当前门店店员 - 保证只有当 clerkStoreId 存在时才执行新增操作 - 更新工作记录文档,补充审核通过流程中新添加的店员记录步骤 - 保持原有升级开发者逻辑和接口调用不变" />
|
||||
<MESSAGE value="feat(user): 补充开发者审核通过时添加店员记录功能 - 引入 addShopStoreUser 接口用于新增门店店员记录 - 新增 clerkStoreId 和 clerkTenantId 状态,保存当前审核店员的门店和租户信息 - 在审核通过后调用 addShopStoreUser,添加申请人为当前门店店员 - 保证只有当 clerkStoreId 存在时才执行新增操作 - 调整平台管理页 UI,使其与门店中心保持一致风格 - 门店中心及平台管理入口权限调整,仅超级管理员可访问 - 用户菜单中平台管理和门店中心入口现基于 isSuperAdmin 字段控制 - 优化登录及权限校验流程,避免权限不足时页面闪烁显示 - 更新工作记录文档,补充审核通过流程中新添加的店员记录步骤" />
|
||||
<MESSAGE value="feat(user): 合并并切换“申请开发者”菜单项状态 - 在用户菜单中根据 isDeveloper 字段切换显示“申请成为开发者”或“进入开发者中心” - 已通过审核用户直接显示“进入开发者中心”且高亮显示 - 去除冗余的独立“开发者中心”菜单条目,简化菜单结构 - 调整平台管理页和门店中心入口权限为仅超级管理员可访问 - 优化登录与权限校验流程,防止权限不足时页面闪烁或无访问权限空白 - 更新相关文档,完善审核通过后店员记录添加流程" />
|
||||
<MESSAGE value="fix(auth): 统一超级管理员标识为布尔值true - 将所有页面和菜单中超级管理员校验由数字1改为布尔true - 平台管理、门店中心菜单项仅在isSuperAdmin===true时显示 - 门店中心页面移除店员门槛,改为超级管理员访问限制 - 非超级管理员访问门店中心弹窗提示并返回 - 首页工作台入口及相关访问权限判定切换为布尔判断 - 优化首屏加载状态,避免权限校验闪烁和误判" />
|
||||
<MESSAGE value="feat(user): 合并切换开发者菜单项状态并更新开发者中心UI - 用户菜单中根据 isDeveloper 字段条件展示“申请成为开发者”或“进入开发者中心” - 已审核通过用户菜单显示“进入开发者中心”,并设置高亮样式 - 去除冗余的独立“开发者中心”菜单条目,简化菜单结构 - 重新设计开发者中心页面UI,统一为门店中心风格 - 顶部展示开发者头像及昵称手机号信息 - 功能卡片展示“我的应用”、“API Key”和“我的工单”,异步加载数量角标 - 底部添加“开发者帮助与支持”入口,替代门店订阅消息卡片 - 未登录或非开发者状态显示相应空态提示及操作按钮 - 优化登录状态与权限校验逻辑,防止页面闪烁和无权限内容显示" />
|
||||
<MESSAGE value="feat(user): 合并切换开发者菜单并优化首页轮播图显示 - 用户菜单根据 isDeveloper 字段切换显示申请或进入开发者中心 - 已审核用户菜单高亮显示“进入开发者中心” - 去除冗余开发者中心菜单条目,简化结构 - 重新设计开发者中心页面UI,统一门店中心风格 - 顶部展示开发者头像及昵称手机号信息 - 功能卡片展示“我的应用”、“API Key”和“我的工单”,异步加载数量角标 - 底部添加“开发者帮助与支持”入口,替代门店订阅消息卡片 - 未登录或非开发者状态显示相应空态及操作按钮 - 首页轮播图为空时不显示容器,功能入口自动上移,优化空状态体验 - 优化登录状态与权限校验逻辑,防止页面闪烁和无权限展示" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="feat(user): 合并切换开发者菜单并优化首页轮播图显示 - 用户菜单根据 isDeveloper 字段切换显示申请或进入开发者中心 - 已审核用户菜单高亮显示“进入开发者中心” - 去除冗余开发者中心菜单条目,简化结构 - 重新设计开发者中心页面UI,统一门店中心风格 - 顶部展示开发者头像及昵称手机号信息 - 功能卡片展示“我的应用”、“API Key”和“我的工单”,异步加载数量角标 - 底部添加“开发者帮助与支持”入口,替代门店订阅消息卡片 - 未登录或非开发者状态显示相应空态及操作按钮 - 首页轮播图为空时不显示容器,功能入口自动上移,优化空状态体验 - 优化登录状态与权限校验逻辑,防止页面闪烁和无权限展示" />
|
||||
<option name="NON_MODAL_COMMIT_POSTPONE_SLOW_CHECKS" value="false" />
|
||||
</component>
|
||||
</project>
|
||||
15
.workbuddy/memory/2026-07-21.md
Normal file
15
.workbuddy/memory/2026-07-21.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# 2026-07-21
|
||||
|
||||
## VIP 角色绑定改为动态查询(去硬编码)
|
||||
- 问题:`src/pages/user/vip-review/index.tsx` 中 `VIP_ROLE_ID = 2032` 写死,跨租户不适用。
|
||||
- 改动:
|
||||
- `src/api/system/role/model/index.ts`:给 `RoleParam` 增加 `tenantId?: number | string` 字段,支持按租户过滤角色。
|
||||
- `vip-review/index.tsx`:新增 `getCurrentTenantId()`(取自 `Taro.getStorageSync('TenantId') || TenantId`)与 `getVipRoleId()`(调用 `listRoles({ roleCode:'vip', tenantId })` 动态获取 roleId);`assignVipRole` 改用动态 roleId,并在 `addUserRole` 时带上 `tenantId`。
|
||||
- 导入补充:`listRoles` (from `@/api/system/role`)、`TenantId` (from `@/config/app`)。
|
||||
- 约定:VIP 角色以 `roleCode='vip'` 标识,不再依赖固定 roleId。
|
||||
|
||||
## 后端确认(com.gxwebsoft.core)
|
||||
- `GET /api/system/role`:支持 `roleCode` 过滤(`RoleParam.roleCode` 带 `@QueryField(EQ)`)。✅
|
||||
- 租户隔离由 MyBatis-Plus `TenantLineInnerInterceptor` 自动完成(`sys_role` 不在忽略表清单),租户取自请求头 `tenantId`。`PageParam.buildWrapper` 还会**显式跳过** `tenantId` 字段不拼 WHERE,所以前端的 `tenantId` 查询参数不参与过滤——真正隔离靠请求头。`RoleParam.tenantId` 无 `@QueryField`,传了也被忽略,但功能正确(按当前租户返回 VIP 角色)。
|
||||
- ⚠️ 关键 bug:`POST /api/system/user-role`(save) 用 `getLoginUser().getUserId()` **无条件覆盖**请求体里的 `userId`。VIP 审核通过时,角色会被绑到当前登录的店员,而非申请人 `item.userId`。原硬编码版本也存在此问题。需与用户确认是否修复(建议:仅当请求体 userId 为空时才回退到登录用户)。
|
||||
- 路径核对:前端 `SERVER_API_URL='.../api'`,`addUserRole` 命中 `/api/system/user-role`,一致。
|
||||
74
.workbuddy/memory/2026-07-22.md
Normal file
74
.workbuddy/memory/2026-07-22.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# 2026-07-22 工作日志
|
||||
|
||||
## 开发者登录弹窗 UI 美化
|
||||
- 文件:`src/pages/user/about/index.tsx`
|
||||
- 美化了 Input(圆角12px、高度48px、柔和背景色)、Button(蓝色渐变、阴影、圆角14px)、验证码按钮(与输入框等高、倒计时优化)
|
||||
- 弹窗整体:圆角加大、内边距增加、宽度收窄
|
||||
|
||||
## 开发者登录弹窗样式改为红色系
|
||||
- 文件:`src/pages/user/about/index.tsx`
|
||||
- Input 边框:`transparent` → `#e53e3e`(红色),背景 `#f7f8fa` → `#fff5f5`(淡红)
|
||||
- 获取验证码按钮:激活态添加 `backgroundColor: #e53e3e`、`borderColor: #e53e3e`
|
||||
- 登录按钮:渐变从蓝色 `#4f8cff→#3b6fd4` 改为红色 `#fc5c5c→#e53e3e`,阴影同步改为红色
|
||||
|
||||
## 关于我们页面介绍内容改写
|
||||
- 文件:`src/pages/user/about/index.tsx`
|
||||
- 公司:南宁市网宿信息科技有限公司,品牌:威数谱软件(websopy)
|
||||
- 重写品牌故事(三个段落)、核心业务(6项软件相关)、联系我们(公司名称/总部/电话/邮箱/官网)、版权信息
|
||||
- 头部 Logo 文字「鑫」→「威」,版本号显示 websopy v1.0.0
|
||||
|
||||
## VIP 概念替换为「开发者」(文案统一)
|
||||
- 起因:用户要求把 `pages/user/vip-upgrade/index` 的 VIP 概念换成「开发者」。后台审核页 `vip-review` 已定义 `VIP_ROLE_NAME='开发者'`,概念本就一致。
|
||||
- 改动文件与要点:
|
||||
- `vip-upgrade/index.tsx` + `index.config.ts`:导航栏/分享标题/主副标题/状态说明/温馨提示/权益区块标题及权益项描述,全部「VIP会员」→「开发者」;顶部图标 👑→🔰;表单标签「门店名称/门店地址」→「开发团队/公司地址」(placeholder、toast 同步),底层字段 realName/address 不变。
|
||||
- `user.tsx`:用户中心入口「升级VIP会员」👑→「申请开发者」🔰。
|
||||
- `store/center/index.tsx`:门店中心入口「VIP会员审核/审核客户VIP会员申请」👑→「开发者审核/审核客户开发者申请」🔰。
|
||||
- `vip-review/index.tsx`:导航栏标题、通过/驳回弹窗文案、审核说明、列表字段标签(门店名称/门店地址→开发团队/公司地址)统一为「开发者」。
|
||||
- 注意:仅改可见文案与图标,**未重命名**页面路由/文件夹(vip-upgrade、vip-review)、变量名(VIP_ROLE_CODE、pendingVipCount 等)及定价逻辑(dealerPrice/VIP 状态判断仍保留原标识符,因对应后端角色与缓存 key)。
|
||||
- 用户确认方式:先给文案对照表,经 AskUserQuestion 确认表单标签与图标及范围后执行。
|
||||
|
||||
## VIP 升级页新增「所在城市」选择
|
||||
- 文件:`src/pages/user/vip-upgrade/index.tsx`、`src/api/shop/shopDealerApply/model/index.ts`
|
||||
- 在「办公地址」上方新增「所在城市 *」字段:复用微信原生 `Picker mode='region'`(省市区三级),与 `pages/user/address-edit.tsx` 风格一致。
|
||||
- 状态:`cityValue: string[]` + 派生 `cityText`(以空格连接);`Picker` 的 `disabled` 跟随 `isReadonly`(审核中/已通过只读)。
|
||||
- `ShopDealerApply` 模型新增 `city?: string`(注释:省 市 区,空格分隔)。
|
||||
- 提交时 `addShopDealerApply` 增加 `city: cityText`;读取历史申请时 `setCityValue(latest.city.split(' '))`;保存校验增加「请选择所在城市」。
|
||||
- 注意:仅前端扩展字段,后端是否落库 `city` 存疑(模型此前只有 address),需后端 schema 支持才能真正持久化。
|
||||
|
||||
## vip-review 审核通过:改为「更新 user 为 developer」(不新增角色)
|
||||
- 起因:用户要求 `pages/user/vip-review/index` 审核通过时直接把 user 标记为开发者,而不是往 `sys_user_role` 加 developer 角色。
|
||||
- 后端(仓库 `/Users/gxwebsoft/JAVA/com.gxwebsoft.core`,已 `mvn -o compile` BUILD SUCCESS):
|
||||
- `User.isDeveloper`(`sys_user.is_developer`,Boolean)此前从未被写入,正是开发者标记位。
|
||||
- `UserService` 新增 `boolean markAsDeveloper(Integer userId)`;`UserServiceImpl` 用最小实体 `updateById` 置 `isDeveloper=true`,成功后再经 `syncMessageProducer` 同步 websopy(复用 updateUser 的同步范式)。
|
||||
- `UserController` 新增 `PUT /api/system/user/developer/{userId}`(`setDeveloper`),权限沿用 `sys:userRole:save`(与原 `addUserRole` 一致,避免店员 403 回归)。
|
||||
- 前端:
|
||||
- `@/api/system/user/index.ts` 新增 `setUserDeveloper(userId)` → `PUT /system/user/developer/{userId}`。
|
||||
- `vip-review/index.tsx`:移除 `addUserRole/listUserRole/listRoles/TenantId` 等角色相关 import 与 `VIP_ROLE_*`、`getCurrentTenantId`、`getVipRoleId`、`assignVipRole`、`formatDateTime`;`handleApprove` 中 `await assignVipRole(item.userId)` 替换为 `await setUserDeveloper(item.userId!)`。
|
||||
- 约定:**开发者身份 = `sys_user.is_developer` 布尔位,不是角色表记录**(已记项目 MEMORY.md)。
|
||||
|
||||
## vip-review 审核通过:补回「开发者角色绑定」+ 保留 `isDeveloper=1`
|
||||
- 起因:上一轮把 `addUserRole` 整段删了(按"不用新增角色")。本轮用户要求"除了更新角色之外,还要更新 isDeveloper=1",即两者都要。
|
||||
- 改动:仅前端 `src/pages/user/vip-review/index.tsx`:
|
||||
- 恢复 import:`addUserRole, listUserRole`(@/api/system/userRole)、`listRoles`(@/api/system/role)、`TenantId`(@/config/app)。
|
||||
- 恢复常量与函数:`VIP_ROLE_CODE='developer'`、`VIP_ROLE_NAME='开发者'`、`getCurrentTenantId()`、`getVipRoleId()`(按 roleCode+租户查 roleId)、`assignVipRole(userId)`(查已有角色、无则 `addUserRole`)。
|
||||
- `handleApprove` 中 `await setUserDeveloper(item.userId!)` 之前新增 `await assignVipRole(item.userId)`:先绑 developer 角色,再置 `is_developer=1`。
|
||||
- 后端 `markAsDeveloper`(`PUT /system/user/developer/{userId}`)上一轮已落地并编译通过,本次未改;其 `updateById` 置 `isDeveloper=true` 即"更新数据库"的落库动作。
|
||||
- 结论:审核通过 = 绑定 developer 角色 + 置 isDeveloper=1(两项都做)。已更新项目 MEMORY.md 的"开发者身份表示方式"条目。
|
||||
|
||||
## vip-review 审核通过:更正为「替换角色为 developer」+ `is_developer=1`(最终方案)
|
||||
- 用户纠正上一轮:不是"新增/叠加角色"(那样会留下原角色 + developer 两个角色),而是**把用户角色改成 developer(替换,不并存)**,同时 `sys_user.is_developer=1`。
|
||||
- 前端 `vip-review/index.tsx`:撤掉上一轮刚加回的 `assignVipRole` 及 `addUserRole/listUserRole/listRoles/TenantId/VIP_ROLE_*`/`getCurrentTenantId`/`getVipRoleId`;`handleApprove` 恢复为只 `await setUserDeveloper(item.userId!)`。前端完全不碰 `sys_user_role`。
|
||||
- 后端 `UserServiceImpl.markAsDeveloper` 本次新增"替换角色"逻辑:置 `is_developer=1` 后,按 `user.getTenantId()` 调 `roleService.getByRoleCode(roleCode="developer", tenantId)` 找到 developer 角色 → `userRoleService.remove(eq userId)` 删现有角色 → `saveBatch(userId, [developerRoleId])` 只绑 developer(单角色)。整段包 try/catch,角色缺失/失败不影响 is_developer 落库。新增 import:`RoleParam`、`java.util.Collections`。
|
||||
- 编译:`./mvnw -o compile` → BUILD SUCCESS。前端无残留角色符号。
|
||||
- 结论(已同步 MEMORY.md):升级为开发者 = 单角色 developer + isDeveloper 标记位;前端一次 `setUserDeveloper` 调用、后端一处搞定。
|
||||
|
||||
## 小程序端新增开发者中心 & 平台管理(入口改造)
|
||||
- 起因:用户要求小程序端对齐 PC 端 `/developer` 与 `/admin` 功能。方案确认:入口挂「我的」页,分阶段实现 MVP 核心。
|
||||
- 页面(新增于 `src/pages/developer/*` 与 `src/pages/admin/*`,已注册到 `app.config.ts`):
|
||||
- 开发者中心:`index`(概览) / `apps`(我的应用) / `apikeys`(API Key) / `tickets`(我的工单)
|
||||
- 平台管理:`index`(管理首页) / `app-review`(应用审核) / `git-review`(Git/权限审核) / `domain-review`(域名绑定审核) / `tickets`(工单处理)
|
||||
- API 模块(新增 `src/api/app/`:apikey、appProduct、appDomain、ticket、developer),复刻 PC 端 `/api/app/*` 契约,基址走 `WEBSOPY_API_BASE_URL`。
|
||||
- 公共组件:新增 `StatCard` / `EntryGrid` / `CellRow`(`src/components/common/`)。
|
||||
- 入口方式(**本次按用户最新要求改**):不做工作台卡片,改为「我的」页功能菜单里的链接项,与「门店中心」同款——`开发者中心` 仅当 `(user as any)?.isDeveloper` 时显示、`平台管理` 仅当 `(user as any)?.isAdmin` 时显示,均 `requireAuth`。首页不额外加卡。
|
||||
- 关键依赖:菜单显示与否取决于登录接口返回的 user 对象是否携带 `isDeveloper/isAdmin` 字段(后端 `setDeveloper` 写的是 `sys_user.is_developer`,但登录态是否回填该字段需联调确认)。
|
||||
- 编译验证:已 `npm run build:weapp`(后台 IzjQLY 进行中),待确认无报错。
|
||||
86
.workbuddy/memory/2026-07-23.md
Normal file
86
.workbuddy/memory/2026-07-23.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# 2026-07-23
|
||||
|
||||
## 扫码进入小程序后支付页空白 bug
|
||||
|
||||
**现象**:用户从 web 端订单详情生成 QR 码 → 微信扫码进入小程序 → `passport/pay` 页面只显示卡片标题"确认支付"、"支付金额"标签、底部提示文案,**商品名称/价格类型/价格/分隔线/取消按钮/立即支付按钮全部缺失**,无法继续支付流程。
|
||||
|
||||
**根因**:`@nutui/nutui-react-taro@2.7.4` 的 Cell / Price / Divider / Button 内部用 `React.createElement("div", ...)`,而 `dist/base.wxml` 里只有 `<view>` / `<text>` / `<button>` 模板没有 `<div>`。这些组件在 weapp 中"空壳化"。截图里只显示纯 Text/View 和图标(来自 `@nutui/icons-react-taro` 的 SVG),没有显示的内容全部是 nutui-react-taro 组件。
|
||||
|
||||
**修复**:只改 `src/passport/pay/index.tsx` 一个文件,把 4 个 nutui 组件换成 Taro 原生 / 项目自定义组件:
|
||||
|
||||
- `import { Button, Cell, Price, Divider } from '@nutui/nutui-react-taro'` → `import { Button } from '@tarojs/components'` + `import Price from '@/components/common/Price'`
|
||||
- `<Cell>` → `<View>` + `<Text>` 自绘(用 `flex justify-between px-4 py-3 border-b border-gray-100`)
|
||||
- `<Price size="large" thousands>` → `<Price size="large">`(项目自定义组件)
|
||||
- `<Divider>` → `border-b border-gray-100` 替代
|
||||
- `<Button type="primary" block>` → `<Button className="bg-blue-500 text-white rounded-full">`(className 控制颜色)
|
||||
- error/paid/loading 三个分支里的 `<Button type="primary">` 也一并替换
|
||||
|
||||
**验证**:
|
||||
- `npm run build:weapp` → `Compiled successfully in 14.45s`
|
||||
- `dist/passport/pay/index.js` 里 `nut-cell|nut-button|nut-divider|nut-price` 全部为 0;按钮编译成 `l.zx`(Taro Button 局部变量名)
|
||||
- 文件最终 413 行,diff 集中在 import + 渲染区,逻辑(数据获取、JSAPI 支付、状态判断 payStatus vs status)没动
|
||||
|
||||
**遗留**:项目其它页面(passport/{login,forget,register,setting,unified-qr,qr-login}、pages/order/*、pages/booking/*、components/common/{FixedButton,EmptyState}、components/QRLoginScanner)也用了 nutui Button/Checkbox/Input/Card/Tabs/Tag 等。如果以后用户反馈"按钮/输入框看不到或不可点",按同样模式替换即可。这次没改,避免一次改动面太大。
|
||||
|
||||
## vip-review 审核通过补充:添加 shop_store_user 记录
|
||||
|
||||
**背景**:之前实现的 `src/pages/user/vip-review/index.tsx` 审核通过只调了 `updateShopDealerApply` + `setUserDeveloper`,漏了"给申请人加一条门店店员记录"。用户 2026-07-23 指出"审核通过的操作,还是要添加一条记录到 shopStoreUser,可能之前理解错了",要求补上。
|
||||
|
||||
**修改**(仅前端一个文件):
|
||||
- import 增加 `addShopStoreUser`(来自 `@/api/shop/shopStoreUser`)。
|
||||
- 新增 state `clerkStoreId` / `clerkTenantId`,在 `getMyClerk()` 成功后写入(店员记录自带 `storeId`/`tenantId`)。
|
||||
- `handleApprove` 在 `setUserDeveloper` 之后调 `addShopStoreUser({ userId, storeId: clerkStoreId, tenantId: clerkTenantId, isDelete: 0, comments: '开发者审核通过自动添加' })`,仅当 `clerkStoreId` 存在时执行。
|
||||
|
||||
**依赖**:后端 `POST /shop/shop-store-user`(ShopStoreUserController.save,需 `shop:shopStoreUser:save` 权限,店员已有);tenantId 复用审核店员的租户,避免前端另取。
|
||||
|
||||
## 平台管理页 UI 对齐门店中心
|
||||
|
||||
**需求**:用户要求 `pages/admin/index/index` 与 `pages/store/center/index` 一模一样的 UI。
|
||||
|
||||
**改动**(仅 `src/pages/admin/index/index.tsx`):
|
||||
- 删除原先 NavBar + StatCard 网格 + EntryGrid 的旧结构,整体改写为门店中心同款「渐变绿色头部 + 白卡片列表(图标/标题/描述/角标/箭头)+ 底部信息卡」。
|
||||
- 头部展示管理员 avatar + 「平台管理」标题 + 昵称·手机号(取 `useUser().user`)。
|
||||
- FEATURE_CARDS 保留 4 个入口:应用审核(📝)、Git 审核(🐙)、域名审核(🌐)、工单处理(🎫),各自带 desc、color/bgColor、url。
|
||||
- 角标沿用项目 `Badge` 组件:应用审核=待审核应用数(pagePublishReviews 已按 pending_review 过滤),工单处理=未解决/未关闭工单数(getAllTickets 取前100条 client 端过滤)。
|
||||
- 底部卡片「平台概况」保留原页面统计:应用总数、用户总数、工单总数(pageAllApps / pageUsers / getAllTickets 的 total)。
|
||||
- 保留登录校验(!isLoggedIn)与管理员校验(!isAdmin)两个空态,改为居中图标文案,去掉 NavBar 依赖(标题走 definePageConfig)。
|
||||
- 新页面不再引入 nutui 组件,只用 `@tarojs/components` + 自定义 `Badge`,规避 weapp 空白坑。
|
||||
|
||||
**待验证**:`npm run build:weapp` 未实跑(环境 tsc 报 @tarojs/taro 类型库缺失属全局问题,与本次无关)。
|
||||
|
||||
## 平台管理 / 门店中心 改为 isSuperAdmin=true 才显示
|
||||
|
||||
**需求**:平台管理与门店中心两个入口都需要 `isSuperAdmin=true`(布尔)才显示(原先平台管理=isAdmin、门店中心=店员 storeInfo)。门店中心页面本身按用户确认改为「仅超级管理员可进」。
|
||||
|
||||
**改动文件**:
|
||||
- `src/pages/user/user.tsx`:菜单项 平台管理、门店中心 的显示条件均由 `isAdmin` / `storeInfo` 改为 `(user as any)?.isSuperAdmin === 1`。
|
||||
- `src/pages/index/index.tsx`:首页角色工作台外层容器补 `|| isSuperAdmin===true`,平台管理入口 `(user as any)?.isAdmin` → `isSuperAdmin === true`。
|
||||
- `src/pages/admin/index/index.tsx`:页面访问门槛 `isAdmin` → `isSuperAdmin===true`(含空态文案);`loading` 拆为 `userLoading`(来自 useUser,防闪烁) + `dataLoading`(角标数据);首屏 `if (userLoading || dataLoading) spinner`。
|
||||
- `src/pages/store/center/index.tsx`:移除 `getMyClerk()` 店员门槛,改为 `isSuperAdmin===true` 才进;非超管弹「仅超级管理员可访问」并 navigateBack;头部加 `header` 兜底(超管非店员时显示「门店中心」+ 管理员昵称/手机号);渲染守卫 `userLoading → 请先登录 → 无访问权限`,去掉原 `if(!storeInfo) return null`(否则非店员超管整页空白)。
|
||||
|
||||
**验证**:`npm run build:weapp` 在沙箱被 OOM/锁文件杀掉(exit 137,无报错),属环境问题,非代码错误;改动均为项目既有写法(useUser / Badge / 渐变头部)。
|
||||
|
||||
## 「申请成为开发者」菜单项随 isDeveloper 切换
|
||||
|
||||
**需求**:我的页面菜单里「申请成为开发者(推荐)」条目,若用户已审核通过成为开发者,该处文案/链接改为「进入开发者中心」(跳 `/pages/developer/index/index`)。
|
||||
|
||||
**改动**(`src/pages/user/user.tsx` 菜单 `menuItems`):
|
||||
- 把原第 105 行固定「申请成为开发者」+ 第 107 行独立「开发者中心」(仅 isDeveloper 显示) 合并为条件式:`(user as any)?.isDeveloper ? { 进入开发者中心 → /pages/developer/index/index } : { 申请成为开发者 → /pages/user/vip-upgrade/index }`,两者均 `highlight: true`(醒目推荐样式)。去掉了冗余的独立「开发者中心」条目。
|
||||
|
||||
## 开发者中心 UI 对齐门店中心
|
||||
|
||||
**需求**:`pages/developer/index/index` 的 UI 改成和门店中心(`pages/store/center/index`)一模一样。
|
||||
|
||||
**改动**(仅 `src/pages/developer/index/index.tsx`,整体重写):
|
||||
- 删除原 NavBar + StatCard 3列网格 + EntryGrid 老结构,改写为门店中心同款「渐变绿头部 + 白卡片列表(图标/标题/描述/角标/箭头)+ 底部卡片」。
|
||||
- 头部:开发者 avatar + 「开发者中心」标题 + 昵称·手机号(取 `(user as any)`)。
|
||||
- FEATURE_CARDS 三张:我的应用(📦/蓝)、API Key(🔑/绿)、我的工单(🎫/橙),各自 desc + url;角标沿用 `Badge`,展示各功能的计数(stats.apps/keys/tickets,由 getMyApps/getAppApiKeyStats/getMyTickets 的 total 取),卡片渲染后异步填充。
|
||||
- 底部卡片改为「开发者帮助与支持」(💡 蓝圈)→ `/pages/user/help-center/index`(替代门店中心的订阅消息卡,订阅模板是门店专属,开发者无对应模板)。
|
||||
- 保留两个空态:!isLoggedIn「请先登录」、!isDeveloper「尚未加入开发者中心」+ 申请按钮(保留原 apply 入口,store center 无此场景故无按钮);首屏用 `userLoading`(来自 useUser) 转圈防闪烁。
|
||||
- 不再引入 NavBar/StatCard/EntryGrid/Loading(可能内部用 nutui,踩 weapp 空白坑),只用 `@tarojs/components` + 自定义 `Badge`,与门店中心一致。
|
||||
|
||||
## 首页轮播图空时不显示
|
||||
|
||||
**需求**:`pages/index/index` 的轮播图(banner)若为空则不显示。
|
||||
|
||||
**改动**(`src/pages/index/index.tsx` 轮播图渲染段):把外层 `.index-banner-wrap` 容器从「`bannerSlides.length>0` 渲染 Swiper,否则渲染『暂无轮播图』占位框」改为「`bannerSlides.length > 0 &&` 整个容器才渲染」;空时整块(含占位框)不显示,下方功能入口自动上移。
|
||||
12
.workbuddy/memory/2026-07-24.md
Normal file
12
.workbuddy/memory/2026-07-24.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# 2026-07-24 工作日志
|
||||
|
||||
## 应用列表页改版(pages/developer/apps/index)
|
||||
|
||||
将「我的应用」列表从 CellRow 纯文本行改为卡片式布局:
|
||||
- **卡片结构**:左侧 Logo/Emoji 图标 + 应用名 + 类型标识;中部双状态标签(发布状态 + 运行状态,带颜色);底部两行截断描述;底部操作栏(管理后台 / 首页 / 编辑 / 删除)。
|
||||
- **Logo**:`AppProduct.logo` 有值时用 `Image` 展示,否则按 `appType` 显示 emoji 占位(🌐💚🤖🍎⚙️ 等)。
|
||||
- **操作行为**:管理后台/首页按钮 → `setClipboardData` 复制链接并 toast;编辑 → 弹窗预填数据调 `updateAppProduct`;删除 → `Taro.showModal` 确认后调 `deleteAppProduct`。
|
||||
- **编辑模式**:复用创建弹窗,`productCode` 编辑时 disabled,按钮文案区分「创建」/「保存」。
|
||||
- **API 新增**:`appProduct.ts` 加了 `updateAppProduct`(PUT /app/product/update)和 `deleteAppProduct`(DELETE /app/product/delete/{id}),引入 `put`/`del` from request utils。
|
||||
- **空状态**:带 emoji 图标 + 引导文案。
|
||||
- 后端 Controller 在 websopy-java(不是 com.gxwebsoft.core),路径前缀 `/api/app/product`。
|
||||
44
.workbuddy/memory/MEMORY.md
Normal file
44
.workbuddy/memory/MEMORY.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# 项目长期记忆(websopy-taro)
|
||||
|
||||
## 核心约定
|
||||
|
||||
### 开发者身份的表示方式
|
||||
- **开发者身份主校验位 = `sys_user.is_developer` 布尔位**(数据库列 `is_developer`),登录态 `MainController.loginByDeveloperSms` 校验它才允许开发者登录。
|
||||
- 后端仓库:`/Users/gxwebsoft/JAVA/com.gxwebsoft.core`(单 Maven 模块,包名 `com.gxwebsoft` 分包)。
|
||||
- 标记接口:`PUT /api/system/user/developer/{userId}`(`UserController.setDeveloper` → `UserService.markAsDeveloper`),成功后经 `syncMessageProducer` 同步 websopy。
|
||||
- 前端调用:`@/api/system/user` 的 `setUserDeveloper(userId)`。
|
||||
- **vip-review 审核通过流程(用户 2026-07-22 最终确认,2026-07-23 补充)**:前端 `handleApprove` 依次调 ① `updateShopDealerApply(applyStatus=20)`;② `setUserDeveloper(userId)`(不碰 `sys_user_role`);③ **`addShopStoreUser`**——在当前门店下给申请人补一条 `shop_store_user`(店员)记录(`userId=申请人, storeId=审核店员所属门店, tenantId=同店员, isDelete=0`)。后端 `markAsDeveloper` 做两件事:① 把 `sys_user.is_developer` 置 `1`;② **替换**用户角色为 `developer`——`roleService.getByRoleCode(roleCode=developer, tenantId=用户租户)` 找到角色 → 删掉该用户现有 `sys_user_role` → 只绑定 developer 这一个角色,**不与原角色并存**(角色替换包 try/catch,失败不影响 isDeveloper 落库)。即"升级为开发者 = 单角色 developer + isDeveloper 标记位 + 一条门店店员记录",不是新增第二个角色。审核店员的 `storeId/tenantId` 来自 `getMyClerk()` 返回值(存于 `clerkStoreId/clerkTenantId` state)。
|
||||
- 注意:前端 `vip-review` / `vip-upgrade` 等页面的变量名仍残留 `VIP_ROLE_*`、`pendingVipCount`、`dealerPrice` 等旧标识符(仅文案改成了「开发者」),后端角色与缓存 key 仍对应这些旧名,重命名需谨慎。
|
||||
|
||||
### 超级管理员(isSuperAdmin=true)访问门槛(2026-07-23 确认)
|
||||
- 用户对象字段 `isSuperAdmin`(`(user as any)?.isSuperAdmin === true`,布尔 true 才算),前端此前未引用,由后端 `sys_user` 返回。
|
||||
- **平台管理**(`pages/admin/index/index`)与**门店中心**(`pages/store/center/index`)两个入口,**菜单与首页工作台均只在 `isSuperAdmin===true` 时显示**(原先平台管理是 `isAdmin`、门店中心是店员 `storeInfo`)。
|
||||
- 门店中心页面已改为「仅超级管理员可进」:移除原 `getMyClerk()` 店员门槛;非超管访问弹「仅超级管理员可访问」并 `navigateBack`;超管若同时是店员仍会加载门店信息用于头部,否则头部兜底显示「门店中心」+ 管理员昵称/手机号。
|
||||
- 两页都用 `useUser()` 的 `loading`(userLoading)做首屏门卫,避免超管用户未从 storage 解析完时先闪「无访问权限」。注意:`pages/store/center` 的入口曾依赖 `user.tsx` 里的 `storeInfo`(getMyClerk),现已不再用 `storeInfo` 决定菜单可见性。
|
||||
|
||||
### 后端代码风格(com.gxwebsoft.core)
|
||||
- Controller 继承 `BaseController`,统一返回 `ApiResult`(成功 `code=0`,失败 `code=1`),用 `success()/fail()`。
|
||||
- 「按 userId 置一个标志位」的范式:`new User(){{setUserId(); setXxx();}}` → `userService.updateById(u)`(参考 `updateStatus`/`updateRecommend`/`auditUser`)。
|
||||
- 用户变更后如需同步 websopy,经 `syncMessageProducer.sendUserSyncMessage("websopy","UPDATE",user)`(在 `UserServiceImpl` 内,`@Autowired(required=false)` 注入)。
|
||||
- 用户相关代码都在 `com.gxwebsoft.common.system`(entity/service/controller),业务应用数据在 `com.gxwebsoft.websopy`(无 Controller)。
|
||||
|
||||
## ⚠️ Taro 小程序 nutui-react-taro 兼容性陷阱(2026-07-23 确认)
|
||||
|
||||
**问题**:`@nutui/nutui-react-taro@2.7.4` 的 **Cell / Price / Divider / Button** 等展示类组件,内部用 `React.createElement("div", ...)` 构建布局。Taro 4 的 babel/swc 没有把这些 `div` 转成 `view`,而 `dist/base.wxml` 里完全没有 `<div>` 模板,导致这些组件在 weapp 中"空壳化"——外层元素不存在,内层内容(`<div>`)被丢弃。
|
||||
|
||||
**症状**:组件位置完全空白 / 按钮不可见 / 价格显示为空。Text/View 与 `@nutui/icons-react-taro` 的图标(Tips/Check/Close/Clock,基于 SVG)渲染正常。
|
||||
|
||||
**解决办法**(最小改动):在该页面用 Taro 原生组件替代:
|
||||
| nutui(不可用) | Taro 原生 / 项目自定义 |
|
||||
|---|---|
|
||||
| `import { Button, Cell, Price, Divider } from '@nutui/nutui-react-taro'` | `import { Button } from '@tarojs/components'` + `import Price from '@/components/common/Price'` |
|
||||
| `<Cell title="X" description={Y} />` | `<View className="flex justify-between px-4 py-3 border-b border-gray-100"><Text className="text-gray-600">X</Text><Text className="text-gray-800">{Y}</Text></View>` |
|
||||
| `<Price price={n} size="large" thousands />` | `<Price price={n} size="large" />`(**用项目自定义 Price**,不用 nutui 的) |
|
||||
| `<Divider />` | 用 `border-b border-gray-100` 替代 |
|
||||
| `<Button type="primary" block loading={x} onClick={y}>...</Button>` | `<Button loading={x} onClick={y} className="flex-1 bg-blue-500 text-white rounded-full border-0">...</Button>`(className 控制颜色,`loading` prop 仍然支持) |
|
||||
|
||||
**适用范围判断**:项目里其它页面(qr-login、register、passport/forget 等)也用了 nutui 的 Cell/Card/Divider/Button,遇到同类"组件消失"现象时按同样模式替换即可。已修:`src/passport/pay/index.tsx`(2026-07-23,扫码进入小程序后支付页空白的 bug)。
|
||||
|
||||
**验证手段**:跑 `npm run build:weapp` 后在 `dist/<page>/index.js` 里 grep `nut-cell|nut-button|nut-divider|nut-price`,应为 0;grep `l.zx`(Taro Button 在 weapp 编译后的局部变量名)应出现 2 次(取消 + 立即支付)。
|
||||
|
||||
**已知的 nutui 组件清单**(按 2026-07-23 grep 结果):`Button` 在 `passport/pay, passport/login/setting/forget/register, pages/order/*, pages/booking/*, components/common/{FixedButton,EmptyState}` 等大量页面;`Cell` 在 `passport/pay, pages/order/order.tsx`;`Divider` 在 `passport/pay, passport/qr-login`;`Price` 在 `passport/pay`。修复顺序建议:pay(已修)→ 视觉影响最大的页面 → 批量。
|
||||
@@ -5,21 +5,21 @@ export const ENV_CONFIG = {
|
||||
API_BASE_URL: 'https://shop-api.websoft.top/api',
|
||||
SERVER_API_URL: 'https://server.websoft.top/api',
|
||||
WEBSOPY_API_BASE_URL: 'https://websopy-api.websoft.top/api',
|
||||
APP_NAME: '威硕谱软件',
|
||||
APP_NAME: '威数谱软件',
|
||||
DEBUG: 'true',
|
||||
},
|
||||
test: {
|
||||
API_BASE_URL: 'https://shop-api.websoft.top/api',
|
||||
SERVER_API_URL: 'https://server.websoft.top/api',
|
||||
WEBSOPY_API_BASE_URL: 'https://websopy-api.websoft.top/api',
|
||||
APP_NAME: '威硕谱软件',
|
||||
APP_NAME: '威数谱软件',
|
||||
DEBUG: 'true',
|
||||
},
|
||||
production: {
|
||||
API_BASE_URL: 'https://shop-api.websoft.top/api',
|
||||
SERVER_API_URL: 'https://server.websoft.top/api',
|
||||
WEBSOPY_API_BASE_URL: 'https://websopy-api.websoft.top/api',
|
||||
APP_NAME: '威硕谱软件',
|
||||
APP_NAME: '威数谱软件',
|
||||
DEBUG: 'false',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
},
|
||||
"compileType": "miniprogram",
|
||||
"appid": "wx541db955e7a62709",
|
||||
"projectname": "威硕谱软件",
|
||||
"projectname": "威数谱软件",
|
||||
"condition": {},
|
||||
"simulatorPluginLibVersion": {},
|
||||
"packOptions": {
|
||||
|
||||
49
src/api/app/apikey.ts
Normal file
49
src/api/app/apikey.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
// API Key 管理(对齐 PC /developer/apikeys,基址走 websopy-api)
|
||||
import { get, post, put, del } from '@/utils/request'
|
||||
import { WebsopyBaseUrl } from '@/config/app'
|
||||
|
||||
const BASE = '/app/apikey'
|
||||
|
||||
export interface AppApiKey {
|
||||
id: number
|
||||
name: string
|
||||
apiKey: string
|
||||
keyPrefix?: string
|
||||
status: number // 0正常 1禁用
|
||||
scopes?: string
|
||||
expireTime?: string
|
||||
lastUsedAt?: string
|
||||
usageCount?: number
|
||||
remark?: string
|
||||
createTime?: string
|
||||
}
|
||||
|
||||
export async function pageAppApiKey(params: any) {
|
||||
const res = await get<any>(BASE, params, { baseUrl: WebsopyBaseUrl })
|
||||
if (res?.code === 0) return res.data
|
||||
return Promise.reject(new Error(res?.message || '加载失败'))
|
||||
}
|
||||
|
||||
export async function createAppApiKey(data: any) {
|
||||
const res = await post<any>(BASE, data, { baseUrl: WebsopyBaseUrl })
|
||||
if (res?.code === 0) return res.data
|
||||
return Promise.reject(new Error(res?.message || '创建失败'))
|
||||
}
|
||||
|
||||
export async function updateAppApiKeyStatus(id: number, status: number) {
|
||||
const res = await put<any>(`${BASE}/${id}/status?status=${status}`, {}, { baseUrl: WebsopyBaseUrl })
|
||||
if (res?.code === 0) return res.message
|
||||
return Promise.reject(new Error(res?.message || '操作失败'))
|
||||
}
|
||||
|
||||
export async function removeAppApiKey(id: number) {
|
||||
const res = await del<any>(`${BASE}/${id}`, {}, { baseUrl: WebsopyBaseUrl })
|
||||
if (res?.code === 0) return res.message
|
||||
return Promise.reject(new Error(res?.message || '删除失败'))
|
||||
}
|
||||
|
||||
export async function getAppApiKeyStats() {
|
||||
const res = await get<any>(`${BASE}/stats`, undefined, { baseUrl: WebsopyBaseUrl })
|
||||
if (res?.code === 0) return res.data
|
||||
return Promise.reject(new Error(res?.message || '加载失败'))
|
||||
}
|
||||
58
src/api/app/appDomain.ts
Normal file
58
src/api/app/appDomain.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
// 个性域名绑定(对齐 PC /admin/domain-bindings,基址走 websopy-api)
|
||||
import { get, post } from '@/utils/request'
|
||||
import { WebsopyBaseUrl } from '@/config/app'
|
||||
|
||||
const BASE = '/app/domain'
|
||||
|
||||
export interface AppDomain {
|
||||
id?: number
|
||||
tenantId?: number
|
||||
productId?: number
|
||||
domain?: string
|
||||
targetIp?: string
|
||||
recordType?: string
|
||||
status?: number // 0待解析 1校验中 2已生效 3已拒绝 4已停用
|
||||
dnsStatus?: number
|
||||
dnsIp?: string
|
||||
proxyConfigured?: number
|
||||
rejectReason?: string
|
||||
tenantName?: string
|
||||
createTime?: string
|
||||
}
|
||||
|
||||
/** 管理端列表 */
|
||||
export async function pageAppDomain(params: any) {
|
||||
const res = await get<any>(`${BASE}/admin/list`, params, { baseUrl: WebsopyBaseUrl })
|
||||
if (res?.code === 0) return res.data
|
||||
return Promise.reject(new Error(res?.message || '加载失败'))
|
||||
}
|
||||
|
||||
export async function verifyDomain(id: number) {
|
||||
const res = await post<any>(`${BASE}/verify/${id}`, {}, { baseUrl: WebsopyBaseUrl })
|
||||
if (res?.code === 0) return res.data
|
||||
return Promise.reject(new Error(res?.message || '校验失败'))
|
||||
}
|
||||
|
||||
export async function setProxy(id: number) {
|
||||
const res = await post<any>(`${BASE}/proxy/${id}`, {}, { baseUrl: WebsopyBaseUrl })
|
||||
if (res?.code === 0) return res.data
|
||||
return Promise.reject(new Error(res?.message || '操作失败'))
|
||||
}
|
||||
|
||||
export async function activeDomain(id: number) {
|
||||
const res = await post<any>(`${BASE}/active/${id}`, {}, { baseUrl: WebsopyBaseUrl })
|
||||
if (res?.code === 0) return res.data
|
||||
return Promise.reject(new Error(res?.message || '操作失败'))
|
||||
}
|
||||
|
||||
export async function rejectDomain(id: number, reason: string) {
|
||||
const res = await post<any>(`${BASE}/reject/${id}`, { reason }, { baseUrl: WebsopyBaseUrl })
|
||||
if (res?.code === 0) return res.message
|
||||
return Promise.reject(new Error(res?.message || '操作失败'))
|
||||
}
|
||||
|
||||
export async function disableDomain(id: number) {
|
||||
const res = await post<any>(`${BASE}/disable/${id}`, {}, { baseUrl: WebsopyBaseUrl })
|
||||
if (res?.code === 0) return res.message
|
||||
return Promise.reject(new Error(res?.message || '操作失败'))
|
||||
}
|
||||
90
src/api/app/appProduct.ts
Normal file
90
src/api/app/appProduct.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
// 应用产品(对齐 PC /developer/apps 与 /admin 应用管理,基址走 websopy-api)
|
||||
import { get, post, put, del } from '@/utils/request'
|
||||
import { WebsopyBaseUrl } from '@/config/app'
|
||||
|
||||
const BASE = '/app/product'
|
||||
|
||||
export interface AppProduct {
|
||||
productId?: number
|
||||
productName?: string
|
||||
productCode?: string
|
||||
appType?: number
|
||||
description?: string
|
||||
logo?: string
|
||||
publishStatus?: string
|
||||
status?: number
|
||||
adminUrl?: string
|
||||
homeUrl?: string
|
||||
createTime?: string
|
||||
userId?: number
|
||||
tenantId?: number
|
||||
rejectReason?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/** 我的应用(创建者) */
|
||||
export async function getMyApps(params: any) {
|
||||
const res = await get<any>(`${BASE}/my/page`, params, { baseUrl: WebsopyBaseUrl })
|
||||
if (res?.code === 0) return res.data
|
||||
return Promise.reject(new Error(res?.message || '加载失败'))
|
||||
}
|
||||
|
||||
/** 开发者应用列表 */
|
||||
export async function getDeveloperApps(params?: any) {
|
||||
const res = await get<any>(`${BASE}/list`, params, { baseUrl: WebsopyBaseUrl })
|
||||
if (res?.code === 0) return res.data
|
||||
return Promise.reject(new Error(res?.message || '加载失败'))
|
||||
}
|
||||
|
||||
/** 创建应用 */
|
||||
export async function addAppProduct(data: any) {
|
||||
const res = await post<any>(`${BASE}/create`, data, { baseUrl: WebsopyBaseUrl })
|
||||
if (res?.code === 0) return res.data
|
||||
return Promise.reject(new Error(res?.message || '创建失败'))
|
||||
}
|
||||
|
||||
/** 发布审核列表(管理端,publishStatus=pending_review) */
|
||||
export async function pagePublishReviews(params: any) {
|
||||
const res = await get<any>(`${BASE}/page`, { ...params, publishStatus: 'pending_review' }, { baseUrl: WebsopyBaseUrl })
|
||||
if (res?.code === 0) return res.data
|
||||
return Promise.reject(new Error(res?.message || '加载失败'))
|
||||
}
|
||||
|
||||
/** 全部应用(管理端) */
|
||||
export async function pageAllApps(params: any) {
|
||||
const res = await get<any>(`${BASE}/page`, params, { baseUrl: WebsopyBaseUrl })
|
||||
if (res?.code === 0) return res.data
|
||||
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 })
|
||||
if (res?.code === 0) return res.message
|
||||
return Promise.reject(new Error(res?.message || '操作失败'))
|
||||
}
|
||||
|
||||
/** 审核拒绝 */
|
||||
export async function rejectPublishReview(params: { productId: number; rejectReason: string }) {
|
||||
const res = await post<any>(
|
||||
`${BASE}/reject/${params.productId}?reason=${encodeURIComponent(params.rejectReason)}`,
|
||||
{},
|
||||
{ baseUrl: WebsopyBaseUrl }
|
||||
)
|
||||
if (res?.code === 0) return res.message
|
||||
return Promise.reject(new Error(res?.message || '操作失败'))
|
||||
}
|
||||
106
src/api/app/developer.ts
Normal file
106
src/api/app/developer.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
// 开发者权限申请 & Git 账号(对齐 PC /developer/requests、/git 与 /admin/git-review,基址走 websopy-api)
|
||||
import { get, post, put } from '@/utils/request'
|
||||
import { WebsopyBaseUrl } from '@/config/app'
|
||||
|
||||
const BASE = '/app/developer'
|
||||
|
||||
export interface PermissionRequest {
|
||||
id?: number
|
||||
userId?: number
|
||||
gitUsername?: string
|
||||
repo?: string
|
||||
repoName?: string
|
||||
reason?: string
|
||||
status?: string // pending / approved / rejected
|
||||
createTime?: string
|
||||
reviewedAt?: string
|
||||
reviewerName?: string
|
||||
rejectReason?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface GitAccount {
|
||||
id?: number
|
||||
userId?: number
|
||||
username?: string
|
||||
email?: string
|
||||
status?: string // pending / verified / rejected
|
||||
verificationNote?: string
|
||||
tenantId?: number
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
// ============ 权限申请 ============
|
||||
export async function pagePermissionRequests(params: any) {
|
||||
const res = await get<any>(`${BASE}/permission-requests`, params, { baseUrl: WebsopyBaseUrl })
|
||||
if (res?.code === 0) return res.data
|
||||
return Promise.reject(new Error(res?.message || '加载失败'))
|
||||
}
|
||||
|
||||
export async function getPermissionRequestStats() {
|
||||
const res = await get<any>(`${BASE}/permission-requests/stats`, undefined, { baseUrl: WebsopyBaseUrl })
|
||||
if (res?.code === 0) return res.data
|
||||
return Promise.reject(new Error(res?.message || '加载失败'))
|
||||
}
|
||||
|
||||
export async function getAvailableRepos() {
|
||||
const res = await get<any>(`${BASE}/available-repos`, undefined, { baseUrl: WebsopyBaseUrl })
|
||||
if (res?.code === 0) return res.data
|
||||
return Promise.reject(new Error(res?.message || '加载失败'))
|
||||
}
|
||||
|
||||
export async function submitPermissionRequest(data: any) {
|
||||
const res = await post<any>(`${BASE}/permission-requests`, data, { baseUrl: WebsopyBaseUrl })
|
||||
if (res?.code === 0) return res.data
|
||||
return Promise.reject(new Error(res?.message || '提交失败'))
|
||||
}
|
||||
|
||||
export async function approvePermissionRequest(id: number) {
|
||||
const res = await put<any>(`${BASE}/permission-requests/${id}/approve`, {}, { baseUrl: WebsopyBaseUrl })
|
||||
if (res?.code === 0) return res.message
|
||||
return Promise.reject(new Error(res?.message || '操作失败'))
|
||||
}
|
||||
|
||||
export async function rejectPermissionRequest(id: number, reason: string) {
|
||||
const res = await put<any>(`${BASE}/permission-requests/${id}/reject`, { reason }, { baseUrl: WebsopyBaseUrl })
|
||||
if (res?.code === 0) return res.message
|
||||
return Promise.reject(new Error(res?.message || '操作失败'))
|
||||
}
|
||||
|
||||
// ============ Git 账号 ============
|
||||
export async function saveGitAccount(data: any) {
|
||||
const res = await post<any>(`${BASE}/git-account`, data, { baseUrl: WebsopyBaseUrl })
|
||||
if (res?.code === 0) return res.data
|
||||
return Promise.reject(new Error(res?.message || '提交失败'))
|
||||
}
|
||||
|
||||
export async function getGitAccountStatus() {
|
||||
const res = await get<any>(`${BASE}/git-account/status`, undefined, { baseUrl: WebsopyBaseUrl })
|
||||
if (res?.code === 0) return res.data
|
||||
return Promise.reject(new Error(res?.message || '加载失败'))
|
||||
}
|
||||
|
||||
export async function getGiteaInfo() {
|
||||
const res = await get<any>(`${BASE}/gitea-info`, undefined, { baseUrl: WebsopyBaseUrl })
|
||||
if (res?.code === 0) return res.data
|
||||
return Promise.reject(new Error(res?.message || '加载失败'))
|
||||
}
|
||||
|
||||
// ============ 管理端 ============
|
||||
export async function pageGitAccounts(params: any) {
|
||||
const res = await get<any>(`${BASE}/git-accounts`, params, { baseUrl: WebsopyBaseUrl })
|
||||
if (res?.code === 0) return res.data
|
||||
return Promise.reject(new Error(res?.message || '加载失败'))
|
||||
}
|
||||
|
||||
export async function approveGitAccount(id: number) {
|
||||
const res = await put<any>(`${BASE}/git-accounts/${id}/approve`, {}, { baseUrl: WebsopyBaseUrl })
|
||||
if (res?.code === 0) return res.message
|
||||
return Promise.reject(new Error(res?.message || '操作失败'))
|
||||
}
|
||||
|
||||
export async function rejectGitAccount(id: number, reason: string) {
|
||||
const res = await put<any>(`${BASE}/git-accounts/${id}/reject`, { reason }, { baseUrl: WebsopyBaseUrl })
|
||||
if (res?.code === 0) return res.message
|
||||
return Promise.reject(new Error(res?.message || '操作失败'))
|
||||
}
|
||||
82
src/api/app/ticket.ts
Normal file
82
src/api/app/ticket.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
// 工单(对齐 PC /developer/tickets 与 /admin/tickets,基址走 websopy-api)
|
||||
import { get, post } from '@/utils/request'
|
||||
import { WebsopyBaseUrl } from '@/config/app'
|
||||
|
||||
const BASE = '/app/ticket'
|
||||
|
||||
export interface Ticket {
|
||||
ticketId?: number
|
||||
ticketNo?: string
|
||||
title?: string
|
||||
productId?: number
|
||||
productName?: string
|
||||
status?: string
|
||||
priority?: string
|
||||
category?: string
|
||||
assigneeName?: string
|
||||
submitUserName?: string
|
||||
replyCount?: number
|
||||
hasUnread?: boolean
|
||||
createTime?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export async function getMyTickets(params: any) {
|
||||
const res = await get<any>(`${BASE}/my`, params, { baseUrl: WebsopyBaseUrl })
|
||||
if (res?.code === 0) return res.data
|
||||
return Promise.reject(new Error(res?.message || '加载失败'))
|
||||
}
|
||||
|
||||
export async function getAllTickets(params: any) {
|
||||
const res = await get<any>(BASE, params, { baseUrl: WebsopyBaseUrl })
|
||||
if (res?.code === 0) return res.data
|
||||
return Promise.reject(new Error(res?.message || '加载失败'))
|
||||
}
|
||||
|
||||
export async function getTicketDetail(id: number) {
|
||||
const res = await get<any>(`${BASE}/${id}`, undefined, { baseUrl: WebsopyBaseUrl })
|
||||
if (res?.code === 0) return res.data
|
||||
return Promise.reject(new Error(res?.message || '加载失败'))
|
||||
}
|
||||
|
||||
export async function getTicketReplies(id: number) {
|
||||
const res = await get<any>(`${BASE}/${id}/replies`, undefined, { baseUrl: WebsopyBaseUrl })
|
||||
if (res?.code === 0) return res.data
|
||||
return Promise.reject(new Error(res?.message || '加载失败'))
|
||||
}
|
||||
|
||||
export async function submitTicket(data: any) {
|
||||
const res = await post<any>(BASE, data, { baseUrl: WebsopyBaseUrl })
|
||||
if (res?.code === 0) return res.data
|
||||
return Promise.reject(new Error(res?.message || '提交失败'))
|
||||
}
|
||||
|
||||
export async function replyTicket(id: number, content: string) {
|
||||
const res = await post<any>(`${BASE}/${id}/reply`, { content }, { baseUrl: WebsopyBaseUrl })
|
||||
if (res?.code === 0) return res.data
|
||||
return Promise.reject(new Error(res?.message || '回复失败'))
|
||||
}
|
||||
|
||||
export async function updateTicketStatus(id: number, status: string) {
|
||||
const res = await post<any>(`${BASE}/${id}/status`, { status }, { baseUrl: WebsopyBaseUrl })
|
||||
if (res?.code === 0) return res.message
|
||||
return Promise.reject(new Error(res?.message || '操作失败'))
|
||||
}
|
||||
|
||||
export async function assignTicket(id: number, assigneeId: number) {
|
||||
const res = await post<any>(`${BASE}/${id}/assign`, { assigneeId }, { baseUrl: WebsopyBaseUrl })
|
||||
if (res?.code === 0) return res.message
|
||||
return Promise.reject(new Error(res?.message || '操作失败'))
|
||||
}
|
||||
|
||||
export async function getTicketStats() {
|
||||
const res = await get<any>(`${BASE}/stats`, undefined, { baseUrl: WebsopyBaseUrl })
|
||||
if (res?.code === 0) return res.data
|
||||
return Promise.reject(new Error(res?.message || '加载失败'))
|
||||
}
|
||||
|
||||
export async function getTechStaffList() {
|
||||
const res = await get<any>(`${BASE}/staffs`, undefined, { baseUrl: WebsopyBaseUrl })
|
||||
if (res?.code === 0) return res.data
|
||||
return Promise.reject(new Error(res?.message || '加载失败'))
|
||||
}
|
||||
@@ -32,6 +32,8 @@ export interface ShopDealerApply {
|
||||
updateTime?: string;
|
||||
// 门店名称
|
||||
merchantName?: string;
|
||||
// 所在城市(省 市 区,以空格分隔)
|
||||
city?: string;
|
||||
// 门店地址
|
||||
address?: string;
|
||||
}
|
||||
|
||||
@@ -24,4 +24,6 @@ export interface RoleParam extends PageParam {
|
||||
roleName?: string;
|
||||
roleCode?: string;
|
||||
comments?: string;
|
||||
// 租户ID(多租户隔离:按租户查询角色)
|
||||
tenantId?: number | string;
|
||||
}
|
||||
|
||||
@@ -115,6 +115,20 @@ export async function updateUser(data: User) {
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 将用户标记为开发者(审核通过时调用,直接更新用户标记位,不新增角色)
|
||||
*/
|
||||
export async function setUserDeveloper(userId: number) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
SERVER_API_URL + '/system/user/developer/' + userId,
|
||||
{}
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户
|
||||
*/
|
||||
|
||||
@@ -117,6 +117,17 @@ export default {
|
||||
'pages/statistics/dashboard/index',
|
||||
'pages/statistics/sales/index',
|
||||
'pages/statistics/users/index',
|
||||
// 开发者中心
|
||||
'pages/developer/index/index',
|
||||
'pages/developer/apps/index',
|
||||
'pages/developer/apikeys/index',
|
||||
'pages/developer/tickets/index',
|
||||
// 平台管理
|
||||
'pages/admin/index/index',
|
||||
'pages/admin/app-review/index',
|
||||
'pages/admin/git-review/index',
|
||||
'pages/admin/domain-review/index',
|
||||
'pages/admin/tickets/index',
|
||||
],
|
||||
"subpackages": [
|
||||
{
|
||||
|
||||
47
src/components/common/CellRow/index.tsx
Normal file
47
src/components/common/CellRow/index.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { View, Text } from '@tarojs/components'
|
||||
|
||||
interface CellRowProps {
|
||||
title: string
|
||||
subtitle?: string
|
||||
extra?: string
|
||||
statusText?: string
|
||||
statusColor?: string
|
||||
showArrow?: boolean
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
export default function CellRow({
|
||||
title,
|
||||
subtitle,
|
||||
extra,
|
||||
statusText,
|
||||
statusColor,
|
||||
showArrow = true,
|
||||
onClick,
|
||||
}: CellRowProps) {
|
||||
return (
|
||||
<View
|
||||
className='flex items-center justify-between px-4 py-3 bg-white border-b border-gray-50'
|
||||
onClick={onClick}
|
||||
>
|
||||
<View className='flex-1 mr-2'>
|
||||
<View className='flex items-center gap-2 flex-wrap'>
|
||||
<Text className='text-sm text-gray-800'>{title}</Text>
|
||||
{statusText && (
|
||||
<Text
|
||||
className='text-xs px-1.5 py-0.5 rounded-full'
|
||||
style={{ color: statusColor, background: (statusColor || '#999') + '22' }}
|
||||
>
|
||||
{statusText}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
{subtitle && (
|
||||
<Text className='text-xs text-gray-400 mt-1 block'>{subtitle}</Text>
|
||||
)}
|
||||
</View>
|
||||
{extra && <Text className='text-sm text-gray-500 mr-2'>{extra}</Text>}
|
||||
{showArrow && <Text className='text-gray-300'>→</Text>}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
43
src/components/common/EntryGrid/index.tsx
Normal file
43
src/components/common/EntryGrid/index.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
interface EntryItem {
|
||||
icon: string
|
||||
label: string
|
||||
url?: string
|
||||
onClick?: () => void
|
||||
badge?: string
|
||||
}
|
||||
|
||||
interface EntryGridProps {
|
||||
items: EntryItem[]
|
||||
columns?: number
|
||||
}
|
||||
|
||||
export default function EntryGrid({ items, columns = 3 }: EntryGridProps) {
|
||||
return (
|
||||
<View className='bg-white rounded-xl p-2'>
|
||||
<View className='flex flex-wrap'>
|
||||
{items.map((it, i) => (
|
||||
<View
|
||||
key={i}
|
||||
className='flex flex-col items-center justify-center py-3 relative'
|
||||
style={{ width: `${100 / columns}%` }}
|
||||
onClick={() => {
|
||||
if (it.onClick) it.onClick()
|
||||
else if (it.url) Taro.navigateTo({ url: it.url })
|
||||
}}
|
||||
>
|
||||
<Text className='text-2xl mb-1'>{it.icon}</Text>
|
||||
<Text className='text-xs text-gray-600'>{it.label}</Text>
|
||||
{it.badge && (
|
||||
<View className='absolute top-1 right-6 px-1 rounded-full' style={{ background: '#ef4444' }}>
|
||||
<Text className='text-white text-xs'>{it.badge}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
25
src/components/common/StatCard/index.tsx
Normal file
25
src/components/common/StatCard/index.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { View, Text } from '@tarojs/components'
|
||||
|
||||
interface StatCardProps {
|
||||
label: string
|
||||
value: string | number
|
||||
sub?: string
|
||||
subColor?: string
|
||||
color?: string
|
||||
}
|
||||
|
||||
export default function StatCard({ label, value, sub, subColor, color = '#3b82f6' }: StatCardProps) {
|
||||
return (
|
||||
<View className='bg-white rounded-lg p-4'>
|
||||
<Text className='text-sm text-gray-500 mb-2 block'>{label}</Text>
|
||||
<Text className='text-2xl font-bold block' style={{ color }}>
|
||||
{value}
|
||||
</Text>
|
||||
{sub && (
|
||||
<Text className='text-xs mt-1 block' style={{ color: subColor || '#52c41a' }}>
|
||||
{sub}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
120
src/pages/admin/app-review/index.tsx
Normal file
120
src/pages/admin/app-review/index.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { View, Text, ScrollView, Textarea } 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 { pagePublishReviews, approvePublishReview, rejectPublishReview } from '@/api/app/appProduct'
|
||||
import { getPageList, APP_TYPE_NAME, PUBLISH_STATUS_NAME, publishStatusColor } from '@/utils/devcenter'
|
||||
|
||||
definePageConfig({ navigationBarTitleText: '应用审核' })
|
||||
|
||||
const AppReviewPage: React.FC = () => {
|
||||
const { isLoggedIn } = useUser()
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [list, setList] = useState<any[]>([])
|
||||
const [detail, setDetail] = useState<any>(null)
|
||||
const [rejectReason, setRejectReason] = useState('')
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const data = await pagePublishReviews({ page: 1, limit: 50 })
|
||||
setList(getPageList(data))
|
||||
} catch (e) {
|
||||
// ignore
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
if (isLoggedIn) load()
|
||||
}, [isLoggedIn, load])
|
||||
|
||||
const approve = async (item: any) => {
|
||||
try {
|
||||
await approvePublishReview(item.productId)
|
||||
Taro.showToast({ title: '已通过', icon: 'success' })
|
||||
setDetail(null)
|
||||
load()
|
||||
} catch (e: any) {
|
||||
Taro.showToast({ title: e?.message || '操作失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
const reject = async (item: any) => {
|
||||
if (!rejectReason.trim()) {
|
||||
Taro.showToast({ title: '请填写拒绝原因', icon: 'none' })
|
||||
return
|
||||
}
|
||||
try {
|
||||
await rejectPublishReview({ productId: item.productId, rejectReason })
|
||||
Taro.showToast({ title: '已拒绝', icon: 'none' })
|
||||
setDetail(null)
|
||||
setRejectReason('')
|
||||
load()
|
||||
} catch (e: any) {
|
||||
Taro.showToast({ title: e?.message || '操作失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-100'>
|
||||
<NavBar title='应用审核' />
|
||||
<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'>暂无待审核应用</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.developer || ''}`}
|
||||
statusText={PUBLISH_STATUS_NAME[app.publishStatus as string] || '待审核'}
|
||||
statusColor={publishStatusColor(app.publishStatus)}
|
||||
onClick={() => {
|
||||
setRejectReason('')
|
||||
setDetail(app)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
|
||||
{detail && (
|
||||
<View style={{ position: 'fixed', left: 0, top: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.4)', zIndex: 999 }} onClick={() => setDetail(null)}>
|
||||
<View className='absolute bottom-0 left-0 right-0 bg-white rounded-t-2xl p-4' onClick={(e) => e.stopPropagation()}>
|
||||
<Text className='text-base font-medium block mb-1'>{detail.productName}</Text>
|
||||
<Text className='text-xs text-gray-500 block mb-3'>{detail.description || '暂无描述'}</Text>
|
||||
<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={rejectReason}
|
||||
onInput={(e: any) => setRejectReason(e.detail.value)}
|
||||
style={{ height: '70px' }}
|
||||
/>
|
||||
</View>
|
||||
<View className='flex gap-3'>
|
||||
<View className='flex-1 py-2.5 rounded-lg text-center' style={{ background: '#16a34a' }} onClick={() => approve(detail)}>
|
||||
<Text className='text-sm text-white'>通过</Text>
|
||||
</View>
|
||||
<View className='flex-1 py-2.5 rounded-lg text-center' style={{ background: '#dc2626' }} onClick={() => reject(detail)}>
|
||||
<Text className='text-sm text-white'>拒绝</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default AppReviewPage
|
||||
128
src/pages/admin/domain-review/index.tsx
Normal file
128
src/pages/admin/domain-review/index.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { View, Text, ScrollView, Textarea } 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 { pageAppDomain, verifyDomain, activeDomain, rejectDomain, disableDomain } from '@/api/app/appDomain'
|
||||
import { getPageList, DOMAIN_STATUS_NAME, domainStatusColor } from '@/utils/devcenter'
|
||||
|
||||
definePageConfig({ navigationBarTitleText: '域名绑定审核' })
|
||||
|
||||
const DomainReviewPage: React.FC = () => {
|
||||
const { isLoggedIn } = useUser()
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [list, setList] = useState<any[]>([])
|
||||
const [detail, setDetail] = useState<any>(null)
|
||||
const [rejectReason, setRejectReason] = useState('')
|
||||
const [busy, setBusy] = useState(false)
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const data = await pageAppDomain({ page: 1, limit: 50 })
|
||||
setList(getPageList(data))
|
||||
} catch (e) {
|
||||
// ignore
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
if (isLoggedIn) load()
|
||||
}, [isLoggedIn, load])
|
||||
|
||||
const act = async (fn: () => Promise<any>, okMsg: string) => {
|
||||
setBusy(true)
|
||||
try {
|
||||
await fn()
|
||||
Taro.showToast({ title: okMsg, icon: 'none' })
|
||||
setDetail(null)
|
||||
setRejectReason('')
|
||||
load()
|
||||
} catch (e: any) {
|
||||
Taro.showToast({ title: e?.message || '操作失败', icon: 'none' })
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-100'>
|
||||
<NavBar title='域名绑定审核' />
|
||||
<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'>暂无域名申请</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View className='mt-3'>
|
||||
{list.map((d) => (
|
||||
<CellRow
|
||||
key={d.id}
|
||||
title={d.domain || '域名'}
|
||||
subtitle={`${d.tenantName || ''} · ${d.targetIp || ''}`}
|
||||
statusText={DOMAIN_STATUS_NAME[d.status as number]}
|
||||
statusColor={domainStatusColor(d.status)}
|
||||
onClick={() => {
|
||||
setRejectReason('')
|
||||
setDetail(d)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
|
||||
{detail && (
|
||||
<View style={{ position: 'fixed', left: 0, top: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.4)', zIndex: 999 }} onClick={() => setDetail(null)}>
|
||||
<View className='absolute bottom-0 left-0 right-0 bg-white rounded-t-2xl p-4' onClick={(e) => e.stopPropagation()}>
|
||||
<Text className='text-base font-medium block mb-1'>{detail.domain}</Text>
|
||||
<Text className='text-xs text-gray-500 block mb-3'>
|
||||
{detail.tenantName} · 目标IP {detail.targetIp}
|
||||
</Text>
|
||||
<View className='flex flex-wrap gap-2 mb-3'>
|
||||
<View className='px-3 py-1.5 rounded-lg text-sm text-white' style={{ background: '#0ea5e9' }} onClick={() => act(() => verifyDomain(detail.id), '已触发校验')}>
|
||||
<Text>检测解析</Text>
|
||||
</View>
|
||||
<View className='px-3 py-1.5 rounded-lg text-sm text-white' style={{ background: '#16a34a' }} onClick={() => act(() => activeDomain(detail.id), '已生效')}>
|
||||
<Text>通过生效</Text>
|
||||
</View>
|
||||
<View className='px-3 py-1.5 rounded-lg text-sm text-white' style={{ background: '#6b7280' }} onClick={() => act(() => disableDomain(detail.id), '已停用')}>
|
||||
<Text>停用</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className='mb-2'>
|
||||
<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={rejectReason}
|
||||
onInput={(e: any) => setRejectReason(e.detail.value)}
|
||||
style={{ height: '60px' }}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
className='py-2.5 rounded-lg text-center'
|
||||
style={{ background: '#dc2626' }}
|
||||
onClick={() => {
|
||||
if (!rejectReason.trim()) {
|
||||
Taro.showToast({ title: '请填写原因', icon: 'none' })
|
||||
return
|
||||
}
|
||||
act(() => rejectDomain(detail.id, rejectReason), '已拒绝')
|
||||
}}
|
||||
>
|
||||
<Text className='text-sm text-white'>拒绝</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default DomainReviewPage
|
||||
147
src/pages/admin/git-review/index.tsx
Normal file
147
src/pages/admin/git-review/index.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { View, Text, ScrollView, Textarea } 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 {
|
||||
pageGitAccounts,
|
||||
approveGitAccount,
|
||||
rejectGitAccount,
|
||||
pagePermissionRequests,
|
||||
approvePermissionRequest,
|
||||
rejectPermissionRequest,
|
||||
} from '@/api/app/developer'
|
||||
import { getPageList } from '@/utils/devcenter'
|
||||
|
||||
definePageConfig({ navigationBarTitleText: 'Git / 权限审核' })
|
||||
|
||||
const GitReviewPage: React.FC = () => {
|
||||
const { isLoggedIn } = useUser()
|
||||
const [tab, setTab] = useState<'git' | 'perm'>('git')
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [list, setList] = useState<any[]>([])
|
||||
const [detail, setDetail] = useState<any>(null)
|
||||
const [rejectReason, setRejectReason] = useState('')
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const data = tab === 'git' ? await pageGitAccounts({ page: 1, limit: 50 }) : await pagePermissionRequests({ page: 1, limit: 50 })
|
||||
setList(getPageList(data))
|
||||
} catch (e) {
|
||||
// ignore
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [tab])
|
||||
useEffect(() => {
|
||||
if (isLoggedIn) load()
|
||||
}, [isLoggedIn, load])
|
||||
|
||||
const approve = async (item: any) => {
|
||||
try {
|
||||
if (tab === 'git') await approveGitAccount(item.id)
|
||||
else await approvePermissionRequest(item.id)
|
||||
Taro.showToast({ title: '已通过', icon: 'success' })
|
||||
setDetail(null)
|
||||
load()
|
||||
} catch (e: any) {
|
||||
Taro.showToast({ title: e?.message || '操作失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
const reject = async (item: any) => {
|
||||
if (!rejectReason.trim()) {
|
||||
Taro.showToast({ title: '请填写拒绝原因', icon: 'none' })
|
||||
return
|
||||
}
|
||||
try {
|
||||
if (tab === 'git') await rejectGitAccount(item.id, rejectReason)
|
||||
else await rejectPermissionRequest(item.id, rejectReason)
|
||||
Taro.showToast({ title: '已拒绝', icon: 'none' })
|
||||
setDetail(null)
|
||||
setRejectReason('')
|
||||
load()
|
||||
} catch (e: any) {
|
||||
Taro.showToast({ title: e?.message || '操作失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
const titleOf = (item: any) =>
|
||||
tab === 'git' ? item.username || item.gitUsername || 'Git 账号' : item.gitUsername || item.repoName || '权限申请'
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-100'>
|
||||
<NavBar title='Git / 权限审核' />
|
||||
<View className='flex bg-white'>
|
||||
<View
|
||||
className={`flex-1 text-center py-3 text-sm ${tab === 'git' ? 'font-medium border-b-2 text-blue-500' : 'text-gray-500'}`}
|
||||
style={tab === 'git' ? { borderBottomColor: '#2563eb' } : {}}
|
||||
onClick={() => setTab('git')}
|
||||
>
|
||||
<Text>Git 账号</Text>
|
||||
</View>
|
||||
<View
|
||||
className={`flex-1 text-center py-3 text-sm ${tab === 'perm' ? 'font-medium border-b-2 text-blue-500' : 'text-gray-500'}`}
|
||||
style={tab === 'perm' ? { borderBottomColor: '#2563eb' } : {}}
|
||||
onClick={() => setTab('perm')}
|
||||
>
|
||||
<Text>权限申请</Text>
|
||||
</View>
|
||||
</View>
|
||||
<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'>暂无待处理</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View className='mt-3'>
|
||||
{list.map((item) => (
|
||||
<CellRow
|
||||
key={item.id}
|
||||
title={titleOf(item)}
|
||||
subtitle={tab === 'git' ? `状态:${item.status || 'pending'}` : `${item.repoName || item.repo || ''} · ${item.reason || ''}`}
|
||||
onClick={() => {
|
||||
setRejectReason('')
|
||||
setDetail(item)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
|
||||
{detail && (
|
||||
<View style={{ position: 'fixed', left: 0, top: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.4)', zIndex: 999 }} onClick={() => setDetail(null)}>
|
||||
<View className='absolute bottom-0 left-0 right-0 bg-white rounded-t-2xl p-4' onClick={(e) => e.stopPropagation()}>
|
||||
<Text className='text-base font-medium block mb-1'>{titleOf(detail)}</Text>
|
||||
<Text className='text-xs text-gray-500 block mb-3'>{tab === 'git' ? detail.email || '' : detail.reason || ''}</Text>
|
||||
<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={rejectReason}
|
||||
onInput={(e: any) => setRejectReason(e.detail.value)}
|
||||
style={{ height: '70px' }}
|
||||
/>
|
||||
</View>
|
||||
<View className='flex gap-3'>
|
||||
<View className='flex-1 py-2.5 rounded-lg text-center' style={{ background: '#16a34a' }} onClick={() => approve(detail)}>
|
||||
<Text className='text-sm text-white'>通过</Text>
|
||||
</View>
|
||||
<View className='flex-1 py-2.5 rounded-lg text-center' style={{ background: '#dc2626' }} onClick={() => reject(detail)}>
|
||||
<Text className='text-sm text-white'>拒绝</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default GitReviewPage
|
||||
241
src/pages/admin/index/index.tsx
Normal file
241
src/pages/admin/index/index.tsx
Normal file
@@ -0,0 +1,241 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { View, Text, Image } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useUser } from '@/hooks/useUser'
|
||||
import { pageAllApps, pagePublishReviews } from '@/api/app/appProduct'
|
||||
import { getAllTickets } from '@/api/app/ticket'
|
||||
import { pageUsers } from '@/api/system/user'
|
||||
import { getPageTotal, getPageList } from '@/utils/devcenter'
|
||||
import Badge from '@/components/common/Badge'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '平台管理',
|
||||
})
|
||||
|
||||
// 功能卡片定义(与门店中心同款结构:图标 + 标题 + 描述 + 角标 + 跳转)
|
||||
const FEATURE_CARDS = [
|
||||
{
|
||||
key: 'app-review',
|
||||
icon: '📝',
|
||||
title: '应用审核',
|
||||
desc: '审核开发者提交的应用上架',
|
||||
url: '/pages/admin/app-review/index',
|
||||
color: '#3b82f6',
|
||||
bgColor: '#dbeafe',
|
||||
showBadge: true,
|
||||
},
|
||||
{
|
||||
key: 'git-review',
|
||||
icon: '🐙',
|
||||
title: 'Git 审核',
|
||||
desc: '审核 Git 仓库接入申请',
|
||||
url: '/pages/admin/git-review/index',
|
||||
color: '#8b5cf6',
|
||||
bgColor: '#ede9fe',
|
||||
showBadge: false,
|
||||
},
|
||||
{
|
||||
key: 'domain-review',
|
||||
icon: '🌐',
|
||||
title: '域名审核',
|
||||
desc: '审核自定义域名绑定',
|
||||
url: '/pages/admin/domain-review/index',
|
||||
color: '#0891b2',
|
||||
bgColor: '#cffafe',
|
||||
showBadge: false,
|
||||
},
|
||||
{
|
||||
key: 'tickets',
|
||||
icon: '🎫',
|
||||
title: '工单处理',
|
||||
desc: '处理用户提交的工单',
|
||||
url: '/pages/admin/tickets/index',
|
||||
color: '#f59e0b',
|
||||
bgColor: '#fef3c7',
|
||||
showBadge: true,
|
||||
},
|
||||
]
|
||||
|
||||
export default function AdminCenterPage() {
|
||||
const { user, isLoggedIn, loading: userLoading } = useUser()
|
||||
const isSuperAdmin = (user as any)?.isSuperAdmin === true
|
||||
|
||||
const [dataLoading, setDataLoading] = useState(true)
|
||||
const [pendingApps, setPendingApps] = useState(0)
|
||||
const [pendingTickets, setPendingTickets] = useState(0)
|
||||
const [overview, setOverview] = useState({ apps: 0, users: 0, tickets: 0 })
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoggedIn && isSuperAdmin) {
|
||||
load()
|
||||
} else {
|
||||
setDataLoading(false)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isLoggedIn, isSuperAdmin])
|
||||
|
||||
/** 加载角标数量与平台概况 */
|
||||
const load = useCallback(async () => {
|
||||
setDataLoading(true)
|
||||
try {
|
||||
const [appsR, pendR, allR, usersR] = await Promise.allSettled([
|
||||
pageAllApps({ page: 1, limit: 1 }),
|
||||
pagePublishReviews({ page: 1, limit: 1 }),
|
||||
getAllTickets({ page: 1, limit: 100 }),
|
||||
pageUsers({ page: 1, limit: 1 } as any),
|
||||
])
|
||||
|
||||
// 待审核应用(pagePublishReviews 已按 pending_review 过滤)
|
||||
const pendingAppsCount = pendR.status === 'fulfilled' ? getPageTotal(pendR.value) : 0
|
||||
setPendingApps(pendingAppsCount)
|
||||
|
||||
// 工单:总数 + 待处理(未解决/未关闭)
|
||||
const ticketData = allR.status === 'fulfilled' ? allR.value : null
|
||||
const ticketList = ticketData ? getPageList(ticketData) : []
|
||||
const pendingTicketsCount = ticketList.filter(
|
||||
(t: any) => t.status !== 'resolved' && t.status !== 'closed'
|
||||
).length
|
||||
setPendingTickets(pendingTicketsCount)
|
||||
|
||||
const appsTotal = appsR.status === 'fulfilled' ? getPageTotal(appsR.value) : 0
|
||||
const ticketsTotal = ticketData ? getPageTotal(ticketData) : 0
|
||||
const usersTotal = usersR.status === 'fulfilled' ? getPageTotal(usersR.value) : 0
|
||||
setOverview({ apps: appsTotal, users: usersTotal, tickets: ticketsTotal })
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
setDataLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
/** 获取卡片角标数字 */
|
||||
const getBadgeCount = (key: string) => {
|
||||
if (key === 'app-review') return pendingApps
|
||||
if (key === 'tickets') return pendingTickets
|
||||
return 0
|
||||
}
|
||||
|
||||
if (userLoading || dataLoading) {
|
||||
return (
|
||||
<View className='min-h-full bg-gray-50 flex items-center justify-center'>
|
||||
<Text className='text-gray-400'>加载中...</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return (
|
||||
<View className='min-h-full bg-gray-50 flex flex-col items-center justify-center py-24'>
|
||||
<Text className='text-5xl mb-3'>🔐</Text>
|
||||
<Text className='text-base text-gray-700 mb-2'>请先登录</Text>
|
||||
<Text className='text-xs text-gray-400 text-center'>登录后即可进入平台管理</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
if (!isSuperAdmin) {
|
||||
return (
|
||||
<View className='min-h-full bg-gray-50 flex flex-col items-center justify-center py-20 px-10'>
|
||||
<Text className='text-5xl mb-3'>🛡️</Text>
|
||||
<Text className='text-base text-gray-700 mb-2'>无访问权限</Text>
|
||||
<Text className='text-xs text-gray-400 text-center'>平台管理仅对超级管理员开放</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const adminName = (user as any)?.nickname || (user as any)?.username || '平台管理员'
|
||||
const adminPhone = (user as any)?.phone
|
||||
|
||||
return (
|
||||
<View className='min-h-full bg-gray-50'>
|
||||
|
||||
{/* 顶部信息区:管理员头像 + 平台名称(与门店中心同款渐变头部) */}
|
||||
<View
|
||||
className='pt-8 pb-12 px-5 rounded-b-3xl relative overflow-hidden'
|
||||
style={{ background: 'linear-gradient(135deg, #15803d 0%, #22c55e 60%, #4ade80 100%)' }}
|
||||
>
|
||||
<View className='absolute -top-10 -right-10 w-40 h-40 rounded-full opacity-10'
|
||||
style={{ background: 'radial-gradient(circle, #ffffff, transparent)' }} />
|
||||
<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 items-center gap-4'>
|
||||
{/* 管理员头像 */}
|
||||
<View className='w-16 h-16 rounded-full bg-white/20 border-2 border-white/40 overflow-hidden flex-shrink-0'>
|
||||
{((user as any)?.avatar) ? (
|
||||
<Image
|
||||
className='w-full h-full'
|
||||
src={(user as any).avatar}
|
||||
mode='aspectFill'
|
||||
/>
|
||||
) : (
|
||||
<View className='w-full h-full flex items-center justify-center'>
|
||||
<Text className='text-2xl'>👤</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 平台信息 */}
|
||||
<View className='flex-1 min-w-0'>
|
||||
<Text className='text-white text-lg font-bold block truncate'>
|
||||
平台管理
|
||||
</Text>
|
||||
<Text className='text-white text-opacity-80 text-sm mt-1 block truncate'>
|
||||
{adminName}{adminPhone ? ` · ${adminPhone}` : ''}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 功能卡片区(与门店中心同款) */}
|
||||
<View className='mx-3 -mt-6 relative z-20'>
|
||||
|
||||
<View className='flex flex-col gap-3'>
|
||||
{FEATURE_CARDS.map(card => (
|
||||
<View
|
||||
key={card.key}
|
||||
className='bg-white rounded-xl p-4 flex items-center gap-4 active:opacity-80 relative'
|
||||
onClick={() => Taro.navigateTo({ url: card.url })}
|
||||
>
|
||||
{/* 图标 */}
|
||||
<View
|
||||
className='w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0'
|
||||
style={{ background: card.bgColor }}
|
||||
>
|
||||
<Text className='text-2xl'>{card.icon}</Text>
|
||||
</View>
|
||||
|
||||
{/* 文字 */}
|
||||
<View className='flex-1 min-w-0'>
|
||||
<Text className='text-gray-800 text-base font-medium block'>{card.title}</Text>
|
||||
<Text className='text-gray-400 text-xs mt-0.5 block'>{card.desc}</Text>
|
||||
</View>
|
||||
|
||||
{/* 待处理角标 */}
|
||||
{card.showBadge && (
|
||||
<Badge count={getBadgeCount(card.key)} className='absolute -top-1 -right-1' size={20} fontSize={11} fontWeight='bold' />
|
||||
)}
|
||||
|
||||
{/* 箭头 */}
|
||||
<Text className='text-gray-300 text-lg flex-shrink-0'>›</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 底部:平台概况(保留原页面统计信息,沿用同款卡片样式) */}
|
||||
<View className='mx-3 mt-6 mb-6'>
|
||||
<View 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'>
|
||||
<Text className='text-xl'>📊</Text>
|
||||
</View>
|
||||
<View className='flex-1 min-w-0'>
|
||||
<Text className='text-gray-800 text-sm font-medium block'>平台概况</Text>
|
||||
<Text className='text-gray-400 text-xs mt-0.5 block'>
|
||||
应用 {overview.apps} · 用户 {overview.users} · 工单 {overview.tickets}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
146
src/pages/admin/tickets/index.tsx
Normal file
146
src/pages/admin/tickets/index.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { View, Text, ScrollView, Input } 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 { getAllTickets, getTicketReplies, replyTicket, updateTicketStatus } from '@/api/app/ticket'
|
||||
import { getPageList, TICKET_STATUS_NAME, TICKET_PRIORITY_NAME, TICKET_CATEGORY_NAME, ticketStatusColor } from '@/utils/devcenter'
|
||||
|
||||
definePageConfig({ navigationBarTitleText: '工单处理' })
|
||||
|
||||
const AdminTicketsPage: React.FC = () => {
|
||||
const { isLoggedIn } = useUser()
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [list, setList] = useState<any[]>([])
|
||||
const [detail, setDetail] = useState<any>(null)
|
||||
const [replies, setReplies] = useState<any[]>([])
|
||||
const [replyText, setReplyText] = useState('')
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const data = await getAllTickets({ page: 1, limit: 50 })
|
||||
setList(getPageList(data))
|
||||
} catch (e) {
|
||||
// ignore
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
if (isLoggedIn) load()
|
||||
}, [isLoggedIn, load])
|
||||
|
||||
const openDetail = async (item: any) => {
|
||||
setDetail(item)
|
||||
setReplyText('')
|
||||
try {
|
||||
const r = await getTicketReplies(item.ticketId)
|
||||
setReplies(Array.isArray(r) ? r : getPageList(r))
|
||||
} catch (e) {
|
||||
setReplies([])
|
||||
}
|
||||
}
|
||||
const sendReply = async () => {
|
||||
if (!replyText.trim() || !detail) return
|
||||
try {
|
||||
await replyTicket(detail.ticketId, replyText)
|
||||
Taro.showToast({ title: '已回复', icon: 'none' })
|
||||
setReplyText('')
|
||||
openDetail(detail)
|
||||
} catch (e: any) {
|
||||
Taro.showToast({ title: e?.message || '回复失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
const setStatus = async (status: string) => {
|
||||
if (!detail) return
|
||||
try {
|
||||
await updateTicketStatus(detail.ticketId, status)
|
||||
Taro.showToast({ title: '已更新', icon: 'none' })
|
||||
setDetail(null)
|
||||
load()
|
||||
} catch (e: any) {
|
||||
Taro.showToast({ title: e?.message || '操作失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-100'>
|
||||
<NavBar title='工单处理' />
|
||||
<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'>暂无工单</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View className='mt-3'>
|
||||
{list.map((t) => (
|
||||
<CellRow
|
||||
key={t.ticketId}
|
||||
title={t.title || '工单'}
|
||||
subtitle={`#${t.ticketNo || t.ticketId} · ${TICKET_CATEGORY_NAME[t.category as string] || ''} · ${t.submitUserName || ''}`}
|
||||
statusText={TICKET_STATUS_NAME[t.status as string]}
|
||||
statusColor={ticketStatusColor(t.status)}
|
||||
onClick={() => openDetail(t)}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
|
||||
{detail && (
|
||||
<View style={{ position: 'fixed', left: 0, top: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.4)', zIndex: 999 }} onClick={() => setDetail(null)}>
|
||||
<View className='absolute bottom-0 left-0 right-0 bg-white rounded-t-2xl p-4' onClick={(e) => e.stopPropagation()}>
|
||||
<Text className='text-base font-medium block mb-1'>{detail.title}</Text>
|
||||
<Text className='text-xs text-gray-500 block mb-3'>
|
||||
{TICKET_STATUS_NAME[detail.status as string]} · {TICKET_PRIORITY_NAME[detail.priority as string]} ·{' '}
|
||||
{detail.assigneeName ? '处理人 ' + detail.assigneeName : '未分配'}
|
||||
</Text>
|
||||
<ScrollView scrollY style={{ maxHeight: '35vh' }}>
|
||||
{replies.length === 0 ? (
|
||||
<Text className='text-xs text-gray-400'>暂无回复</Text>
|
||||
) : (
|
||||
replies.map((r, i) => (
|
||||
<View key={i} className='bg-gray-100 rounded-lg p-2 mb-2'>
|
||||
<Text className='text-sm text-gray-700'>{r.content || r.replyContent || ''}</Text>
|
||||
<Text className='text-xs text-gray-400 mt-1 block'>
|
||||
{r.userName || r.replierName || ''} · {(r.createTime || '').slice(0, 16)}
|
||||
</Text>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</ScrollView>
|
||||
<View className='flex items-center gap-2 mt-3'>
|
||||
<Input
|
||||
className='flex-1 bg-gray-100 rounded-lg px-3 py-2 text-sm'
|
||||
placeholder='输入回复...'
|
||||
value={replyText}
|
||||
onInput={(e: any) => setReplyText(e.detail.value)}
|
||||
/>
|
||||
<View className='px-4 py-2 rounded-lg' style={{ background: '#2563eb' }} onClick={sendReply}>
|
||||
<Text className='text-white text-sm'>回复</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className='flex gap-3 mt-3'>
|
||||
<View className='flex-1 py-2 rounded-lg text-center' style={{ background: '#16a34a' }} onClick={() => setStatus('resolved')}>
|
||||
<Text className='text-sm text-white'>标记已解决</Text>
|
||||
</View>
|
||||
<View className='flex-1 py-2 rounded-lg text-center' style={{ background: '#6b7280' }} onClick={() => setStatus('closed')}>
|
||||
<Text className='text-sm text-white'>关闭</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className='mt-2 text-center' onClick={() => setDetail(null)}>
|
||||
<Text className='text-xs text-gray-400'>关闭</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default AdminTicketsPage
|
||||
@@ -10,8 +10,8 @@ const MessageDetailPage: React.FC = () => {
|
||||
const [message, setMessage] = useState({
|
||||
id: 1,
|
||||
type: 1,
|
||||
title: '欢迎注册威硕谱软件',
|
||||
content: '感谢您注册威硕谱软件,让我们一起开启美好购物体验!\n\n在威硕谱软件,您可以:\n• 浏览精选商品\n• 享受会员专属优惠\n• 参与精彩活动\n• 获得积分奖励\n\n如有任何问题,欢迎联系客服。',
|
||||
title: '欢迎注册威数谱软件',
|
||||
content: '感谢您注册威数谱软件,让我们一起开启美好购物体验!\n\n在威数谱软件,您可以:\n• 浏览精选商品\n• 享受会员专属优惠\n• 参与精彩活动\n• 获得积分奖励\n\n如有任何问题,欢迎联系客服。',
|
||||
time: '2026-05-12 10:30:00',
|
||||
isRead: true,
|
||||
})
|
||||
|
||||
175
src/pages/developer/apikeys/index.tsx
Normal file
175
src/pages/developer/apikeys/index.tsx
Normal 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
|
||||
383
src/pages/developer/apps/index.tsx
Normal file
383
src/pages/developer/apps/index.tsx
Normal file
@@ -0,0 +1,383 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { View, Text, ScrollView, Input, Textarea, Image } 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 { getMyApps, addAppProduct, updateAppProduct, deleteAppProduct } from '@/api/app/appProduct'
|
||||
import {
|
||||
getPageList,
|
||||
APP_TYPE_NAME,
|
||||
PUBLISH_STATUS_NAME,
|
||||
STATUS_NAME,
|
||||
publishStatusColor,
|
||||
} from '@/utils/devcenter'
|
||||
|
||||
definePageConfig({ navigationBarTitleText: '我的应用' })
|
||||
|
||||
const APP_TYPE_OPTIONS = [
|
||||
{ type: 20, name: '微信小程序' },
|
||||
{ type: 10, name: '网站' },
|
||||
{ type: 60, name: 'Android' },
|
||||
{ type: 70, name: 'iOS' },
|
||||
{ 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<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 {
|
||||
// ignore
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoggedIn) load()
|
||||
}, [isLoggedIn, load])
|
||||
|
||||
const openCreate = () => {
|
||||
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)
|
||||
}
|
||||
|
||||
const submit = async () => {
|
||||
if (!form.productName.trim()) {
|
||||
Taro.showToast({ title: '请填写应用名称', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!editing && !/^[a-z][a-z0-9]*$/.test(form.productCode)) {
|
||||
Taro.showToast({ title: '标识须为小写字母/数字', icon: 'none' })
|
||||
return
|
||||
}
|
||||
setSubmitting(true)
|
||||
try {
|
||||
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' })
|
||||
} 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 className='pb-4'>
|
||||
{loading ? (
|
||||
<Loading />
|
||||
) : list.length === 0 ? (
|
||||
<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='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 }}
|
||||
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
|
||||
className='bg-gray-100 rounded-lg px-3 py-2 text-sm'
|
||||
placeholder='如:威硕商城'
|
||||
value={form.productName}
|
||||
onInput={(e) => setForm({ ...form, productName: e.detail.value })}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View className='mb-3'>
|
||||
<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'>
|
||||
{APP_TYPE_OPTIONS.map((opt) => (
|
||||
<View
|
||||
key={opt.type}
|
||||
className='px-3 py-1.5 rounded-full text-sm'
|
||||
style={form.appType === opt.type ? { background: '#2563eb', color: '#fff' } : { background: '#f3f4f6', color: '#374151' }}
|
||||
onClick={() => setForm({ ...form, appType: opt.type })}
|
||||
>
|
||||
<Text>{opt.name}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</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.description}
|
||||
onInput={(e) => setForm({ ...form, description: e.detail.value })}
|
||||
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)}
|
||||
>
|
||||
<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 ? '提交中...' : editing ? '保存' : '创建'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default MyAppsPage
|
||||
228
src/pages/developer/index/index.tsx
Normal file
228
src/pages/developer/index/index.tsx
Normal file
@@ -0,0 +1,228 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { View, Text, Image } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import Badge from '@/components/common/Badge'
|
||||
import { useUser } from '@/hooks/useUser'
|
||||
import { getMyApps } from '@/api/app/appProduct'
|
||||
import { getAppApiKeyStats } from '@/api/app/apikey'
|
||||
import { getMyTickets } from '@/api/app/ticket'
|
||||
import { getPageTotal } from '@/utils/devcenter'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '开发者中心',
|
||||
})
|
||||
|
||||
// 功能卡片定义(未来新增功能只需在这里加一项)
|
||||
const FEATURE_CARDS = [
|
||||
{
|
||||
key: 'apps',
|
||||
icon: '📦',
|
||||
title: '我的应用',
|
||||
desc: '管理你开发并上架的应用',
|
||||
url: '/pages/developer/apps/index',
|
||||
color: '#3b82f6',
|
||||
bgColor: '#dbeafe',
|
||||
showBadge: true,
|
||||
},
|
||||
{
|
||||
key: 'apikeys',
|
||||
icon: '🔑',
|
||||
title: 'API Key',
|
||||
desc: '查看与管理你的接口密钥',
|
||||
url: '/pages/developer/apikeys/index',
|
||||
color: '#10b981',
|
||||
bgColor: '#dcfce7',
|
||||
showBadge: true,
|
||||
},
|
||||
{
|
||||
key: 'tickets',
|
||||
icon: '🎫',
|
||||
title: '我的工单',
|
||||
desc: '提交与跟踪你的工单',
|
||||
url: '/pages/developer/tickets/index',
|
||||
color: '#f59e0b',
|
||||
bgColor: '#fef3c7',
|
||||
showBadge: true,
|
||||
},
|
||||
]
|
||||
|
||||
export default function DeveloperCenterPage() {
|
||||
const { user, isLoggedIn, loading: userLoading } = useUser()
|
||||
const isDeveloper = !!(user as any)?.isDeveloper
|
||||
const [stats, setStats] = useState({ apps: 0, keys: 0, tickets: 0 })
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoggedIn && isDeveloper) {
|
||||
load()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isLoggedIn, isDeveloper])
|
||||
|
||||
/** 加载我的应用 / API Key / 工单数量 */
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
const [appsR, keysR, ticketsR] = await Promise.allSettled([
|
||||
getMyApps({ page: 1, limit: 1 }),
|
||||
getAppApiKeyStats(),
|
||||
getMyTickets({ page: 1, limit: 1 }),
|
||||
])
|
||||
const apps = appsR.status === 'fulfilled' ? getPageTotal(appsR.value) : 0
|
||||
const keys = keysR.status === 'fulfilled' ? getPageTotal(keysR.value) : 0
|
||||
const tickets = ticketsR.status === 'fulfilled' ? getPageTotal(ticketsR.value) : 0
|
||||
setStats({ apps, keys, tickets })
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}, [])
|
||||
|
||||
/** 获取卡片角标数字 */
|
||||
const getBadgeCount = (key: string) => {
|
||||
if (key === 'apps') return stats.apps
|
||||
if (key === 'apikeys') return stats.keys
|
||||
if (key === 'tickets') return stats.tickets
|
||||
return 0
|
||||
}
|
||||
|
||||
if (userLoading) {
|
||||
return (
|
||||
<View className='min-h-full bg-gray-50 flex items-center justify-center'>
|
||||
<Text className='text-gray-400'>加载中...</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return (
|
||||
<View className='min-h-full bg-gray-50 flex flex-col items-center justify-center py-24'>
|
||||
<Text className='text-5xl mb-3'>🔐</Text>
|
||||
<Text className='text-base text-gray-700 mb-2'>请先登录</Text>
|
||||
<Text className='text-xs text-gray-400 text-center'>登录后方可进入开发者中心</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
if (!isDeveloper) {
|
||||
return (
|
||||
<View className='min-h-full bg-gray-50 flex flex-col items-center justify-center py-20 px-10'>
|
||||
<Text className='text-5xl mb-3'>🛠️</Text>
|
||||
<Text className='text-base text-gray-700 mb-2'>尚未加入开发者中心</Text>
|
||||
<Text className='text-xs text-gray-400 text-center mb-5 leading-relaxed'>
|
||||
开发者中心提供应用管理、API Key、工单等能力,请先申请开发者资质
|
||||
</Text>
|
||||
<View
|
||||
className='px-6 py-2 rounded-full'
|
||||
style={{ background: '#2563eb' }}
|
||||
onClick={() => Taro.navigateTo({ url: '/pages/user/vip-upgrade/index' })}
|
||||
>
|
||||
<Text className='text-white text-sm'>申请成为开发者</Text>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
// 头部展示信息
|
||||
const header = {
|
||||
avatar: (user as any)?.avatar,
|
||||
title: '开发者中心',
|
||||
subtitle: `${((user as any)?.nickname || (user as any)?.username || '')}${((user as any)?.phone) ? ` · ${(user as any).phone}` : ''}`,
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='min-h-full bg-gray-50'>
|
||||
|
||||
{/* 顶部信息区:头像 + 名称 */}
|
||||
<View
|
||||
className='pt-8 pb-12 px-5 rounded-b-3xl relative overflow-hidden'
|
||||
style={{ background: 'linear-gradient(135deg, #15803d 0%, #22c55e 60%, #4ade80 100%)' }}
|
||||
>
|
||||
<View className='absolute -top-10 -right-10 w-40 h-40 rounded-full opacity-10'
|
||||
style={{ background: 'radial-gradient(circle, #ffffff, transparent)' }} />
|
||||
<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 items-center gap-4'>
|
||||
{/* 头像 */}
|
||||
<View className='w-16 h-16 rounded-full bg-white/20 border-2 border-white/40 overflow-hidden flex-shrink-0'>
|
||||
{header.avatar ? (
|
||||
<Image
|
||||
className='w-full h-full'
|
||||
src={header.avatar}
|
||||
mode='aspectFill'
|
||||
/>
|
||||
) : (
|
||||
<View className='w-full h-full flex items-center justify-center'>
|
||||
<Text className='text-2xl'>👤</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 信息 */}
|
||||
<View className='flex-1 min-w-0'>
|
||||
<Text className='text-white text-lg font-bold block truncate'>
|
||||
{header.title}
|
||||
</Text>
|
||||
<Text className='text-white text-opacity-80 text-sm mt-1 block truncate'>
|
||||
{header.subtitle}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 功能卡片区 */}
|
||||
<View className='mx-3 -mt-6 relative z-20'>
|
||||
|
||||
<View className='flex flex-col gap-3'>
|
||||
{FEATURE_CARDS.map(card => (
|
||||
<View
|
||||
key={card.key}
|
||||
className='bg-white rounded-xl p-4 flex items-center gap-4 active:opacity-80 relative'
|
||||
onClick={() => Taro.navigateTo({ url: card.url })}
|
||||
>
|
||||
{/* 图标 */}
|
||||
<View
|
||||
className='w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0'
|
||||
style={{ background: card.bgColor }}
|
||||
>
|
||||
<Text className='text-2xl'>{card.icon}</Text>
|
||||
</View>
|
||||
|
||||
{/* 文字 */}
|
||||
<View className='flex-1 min-w-0'>
|
||||
<Text className='text-gray-800 text-base font-medium block'>{card.title}</Text>
|
||||
<Text className='text-gray-400 text-xs mt-0.5 block'>{card.desc}</Text>
|
||||
</View>
|
||||
|
||||
{/* 数量角标 */}
|
||||
{card.showBadge && (
|
||||
<Badge count={getBadgeCount(card.key)} className='absolute -top-1 -right-1' size={20} fontSize={11} fontWeight='bold' />
|
||||
)}
|
||||
|
||||
{/* 箭头 */}
|
||||
<Text className='text-gray-300 text-lg flex-shrink-0'>›</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 底部提示 */}
|
||||
<View className='mx-3 mt-6 mb-6'>
|
||||
{/* 开发者帮助 */}
|
||||
<View
|
||||
className='bg-white rounded-xl p-4 flex items-center gap-3 active:opacity-80'
|
||||
onClick={() => Taro.navigateTo({ url: '/pages/user/help-center/index' })}
|
||||
>
|
||||
<View className='w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0'
|
||||
style={{ background: '#dbeafe' }}>
|
||||
<Text className='text-xl'>💡</Text>
|
||||
</View>
|
||||
<View className='flex-1 min-w-0'>
|
||||
<Text className='text-gray-800 text-sm font-medium block'>开发者帮助与支持</Text>
|
||||
<Text className='text-gray-400 text-xs mt-0.5 block'>
|
||||
查看接口文档、SDK 与常见问题
|
||||
</Text>
|
||||
</View>
|
||||
<Text className='text-blue-500 text-sm flex-shrink-0'>查看</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
224
src/pages/developer/tickets/index.tsx
Normal file
224
src/pages/developer/tickets/index.tsx
Normal file
@@ -0,0 +1,224 @@
|
||||
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 CellRow from '@/components/common/CellRow'
|
||||
import Loading from '@/components/common/Loading'
|
||||
import { useUser } from '@/hooks/useUser'
|
||||
import { getMyTickets, getTicketReplies, submitTicket, replyTicket } from '@/api/app/ticket'
|
||||
import { getPageList, TICKET_STATUS_NAME, TICKET_PRIORITY_NAME, TICKET_CATEGORY_NAME, ticketStatusColor } from '@/utils/devcenter'
|
||||
|
||||
definePageConfig({ navigationBarTitleText: '我的工单' })
|
||||
|
||||
const CATEGORIES = ['bug', 'feature', 'consultation', 'complaint', 'other']
|
||||
const PRIORITIES = ['urgent', 'high', 'normal', 'low']
|
||||
|
||||
const MyTicketsPage: React.FC = () => {
|
||||
const { isLoggedIn } = useUser()
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [list, setList] = useState<any[]>([])
|
||||
const [showCreate, setShowCreate] = useState(false)
|
||||
const [detail, setDetail] = useState<any>(null)
|
||||
const [replies, setReplies] = useState<any[]>([])
|
||||
const [replyText, setReplyText] = useState('')
|
||||
const [createForm, setCreateForm] = useState({ title: '', category: 'bug', priority: 'normal', description: '' })
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const data = await getMyTickets({ page: 1, limit: 50 })
|
||||
setList(getPageList(data))
|
||||
} catch (e) {
|
||||
// ignore
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoggedIn) load()
|
||||
}, [isLoggedIn, load])
|
||||
|
||||
const openDetail = async (item: any) => {
|
||||
setDetail(item)
|
||||
setReplyText('')
|
||||
try {
|
||||
const r = await getTicketReplies(item.ticketId)
|
||||
setReplies(Array.isArray(r) ? r : getPageList(r))
|
||||
} catch (e) {
|
||||
setReplies([])
|
||||
}
|
||||
}
|
||||
|
||||
const sendReply = async () => {
|
||||
if (!replyText.trim() || !detail) return
|
||||
try {
|
||||
await replyTicket(detail.ticketId, replyText)
|
||||
Taro.showToast({ title: '已回复', icon: 'none' })
|
||||
setReplyText('')
|
||||
openDetail(detail)
|
||||
} catch (e: any) {
|
||||
Taro.showToast({ title: e?.message || '回复失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
const submit = async () => {
|
||||
if (!createForm.title.trim()) {
|
||||
Taro.showToast({ title: '请填写标题', icon: 'none' })
|
||||
return
|
||||
}
|
||||
setSubmitting(true)
|
||||
try {
|
||||
await submitTicket({ ...createForm, content: createForm.description })
|
||||
Taro.showToast({ title: '提交成功', icon: 'success' })
|
||||
setShowCreate(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='我的工单'
|
||||
rightText='提交'
|
||||
onRightClick={() => {
|
||||
setCreateForm({ title: '', category: 'bug', priority: 'normal', description: '' })
|
||||
setShowCreate(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'>暂无工单</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View className='mt-3'>
|
||||
{list.map((t) => (
|
||||
<CellRow
|
||||
key={t.ticketId}
|
||||
title={t.title || '工单'}
|
||||
subtitle={`#${t.ticketNo || t.ticketId} · ${TICKET_CATEGORY_NAME[t.category as string] || ''} · ${(t.createTime || '').slice(0, 10)}`}
|
||||
statusText={TICKET_STATUS_NAME[t.status as string]}
|
||||
statusColor={ticketStatusColor(t.status)}
|
||||
onClick={() => openDetail(t)}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
|
||||
{detail && (
|
||||
<View style={{ position: 'fixed', left: 0, top: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.4)', zIndex: 999 }} onClick={() => setDetail(null)}>
|
||||
<View className='absolute bottom-0 left-0 right-0 bg-white rounded-t-2xl p-4' onClick={(e) => e.stopPropagation()}>
|
||||
<Text className='text-base font-medium block mb-2'>{detail.title}</Text>
|
||||
<Text className='text-xs text-gray-500 block mb-3'>
|
||||
{TICKET_STATUS_NAME[detail.status as string]} · {TICKET_PRIORITY_NAME[detail.priority as string]}
|
||||
</Text>
|
||||
<ScrollView scrollY style={{ maxHeight: '40vh' }}>
|
||||
{replies.length === 0 ? (
|
||||
<Text className='text-xs text-gray-400'>暂无回复</Text>
|
||||
) : (
|
||||
replies.map((r, i) => (
|
||||
<View key={i} className='bg-gray-100 rounded-lg p-2 mb-2'>
|
||||
<Text className='text-sm text-gray-700'>{r.content || r.replyContent || ''}</Text>
|
||||
<Text className='text-xs text-gray-400 mt-1 block'>
|
||||
{r.userName || r.replierName || ''} · {(r.createTime || '').slice(0, 16)}
|
||||
</Text>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</ScrollView>
|
||||
<View className='flex items-center gap-2 mt-3'>
|
||||
<Input
|
||||
className='flex-1 bg-gray-100 rounded-lg px-3 py-2 text-sm'
|
||||
placeholder='输入回复...'
|
||||
value={replyText}
|
||||
onInput={(e) => setReplyText(e.detail.value)}
|
||||
/>
|
||||
<View className='px-4 py-2 rounded-lg' style={{ background: '#2563eb' }} onClick={sendReply}>
|
||||
<Text className='text-white text-sm'>发送</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className='mt-2 text-center' onClick={() => setDetail(null)}>
|
||||
<Text className='text-xs text-gray-400'>关闭</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{showCreate && (
|
||||
<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 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={createForm.title}
|
||||
onInput={(e) => setCreateForm({ ...createForm, title: 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'>
|
||||
{CATEGORIES.map((c) => (
|
||||
<View
|
||||
key={c}
|
||||
className='px-3 py-1 rounded-full text-xs'
|
||||
style={createForm.category === c ? { background: '#2563eb', color: '#fff' } : { background: '#f3f4f6', color: '#374151' }}
|
||||
onClick={() => setCreateForm({ ...createForm, category: c })}
|
||||
>
|
||||
<Text>{TICKET_CATEGORY_NAME[c]}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
<View className='mb-3'>
|
||||
<Text className='text-xs text-gray-500 block mb-1'>优先级</Text>
|
||||
<View className='flex flex-wrap gap-2'>
|
||||
{PRIORITIES.map((p) => (
|
||||
<View
|
||||
key={p}
|
||||
className='px-3 py-1 rounded-full text-xs'
|
||||
style={createForm.priority === p ? { background: '#2563eb', color: '#fff' } : { background: '#f3f4f6', color: '#374151' }}
|
||||
onClick={() => setCreateForm({ ...createForm, priority: p })}
|
||||
>
|
||||
<Text>{TICKET_PRIORITY_NAME[p]}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</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={createForm.description}
|
||||
onInput={(e) => setCreateForm({ ...createForm, description: e.detail.value })}
|
||||
style={{ height: '80px' }}
|
||||
/>
|
||||
</View>
|
||||
<View className='flex gap-3'>
|
||||
<View className='flex-1 py-2.5 rounded-lg bg-gray-100 text-center' onClick={() => setShowCreate(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 MyTicketsPage
|
||||
@@ -48,7 +48,7 @@ const GiftCardPurchasePage: React.FC = () => {
|
||||
<View className='absolute rounded-full' style={{ top: '-20px', right: '-20px', width: '80px', height: '80px', backgroundColor: 'rgba(255,255,255,0.2)' }} />
|
||||
<View className='absolute rounded-full' style={{ bottom: '-32px', left: '-32px', width: '96px', height: '96px', backgroundColor: 'rgba(255,255,255,0.2)' }} />
|
||||
|
||||
<Text className='text-sm opacity-80 block mb-2'>威硕谱软件礼品卡</Text>
|
||||
<Text className='text-sm opacity-80 block mb-2'>威数谱软件礼品卡</Text>
|
||||
<Text className='text-4xl font-bold block mb-3'>
|
||||
¥ {amount || '0'}
|
||||
</Text>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export default {
|
||||
navigationBarTitleText: '威硕谱软件',
|
||||
navigationBarTitleText: '威数谱软件',
|
||||
navigationBarBackgroundColor: '#ffffff',
|
||||
navigationBarTextStyle: 'black',
|
||||
backgroundColor: '#f8f8f8',
|
||||
|
||||
@@ -37,7 +37,7 @@ const IndexPage: React.FC = () => {
|
||||
|
||||
// 首页分享:好友 + 朋友圈 + 复制链接(右上角三个按钮全亮)
|
||||
useShare({
|
||||
title: '威硕谱软件 - 精选好物,实惠到家,等你来逛~',
|
||||
title: '威数谱软件 - 精选好物,实惠到家,等你来逛~',
|
||||
path: '/pages/index/index',
|
||||
enableTimeline: true,
|
||||
enableCopyUrl: true,
|
||||
@@ -255,9 +255,34 @@ const IndexPage: React.FC = () => {
|
||||
</View>
|
||||
|
||||
<ScrollView scrollY style={{ height: scrollHeight }}>
|
||||
{/* 轮播图 */}
|
||||
<View className='mx-3 mt-2 rounded-lg overflow-hidden index-banner-wrap'>
|
||||
{bannerSlides.length > 0 ? (
|
||||
{/* 角色工作台入口:仅开发者 / 平台管理员 / 超级管理员可见 */}
|
||||
{(isLoggedIn && ((user as any)?.isDeveloper || (user as any)?.isAdmin || (user as any)?.isSuperAdmin === true)) && (
|
||||
<View className='mx-3 mt-2 grid grid-cols-2 gap-2'>
|
||||
{(user as any)?.isDeveloper && (
|
||||
<View
|
||||
className='flex items-center justify-center py-3 rounded-lg border border-blue-100'
|
||||
style={{ background: 'linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%)' }}
|
||||
onClick={() => Taro.navigateTo({ url: '/pages/developer/index/index' })}
|
||||
>
|
||||
<Text className='text-lg mr-2'>🛠️</Text>
|
||||
<Text className='text-sm font-medium text-blue-700'>开发者中心</Text>
|
||||
</View>
|
||||
)}
|
||||
{(user as any)?.isSuperAdmin === true && (
|
||||
<View
|
||||
className='flex items-center justify-center py-3 rounded-lg border border-purple-100'
|
||||
style={{ background: 'linear-gradient(135deg, #f5f3ff 0%, #ede9fe 100%)' }}
|
||||
onClick={() => Taro.navigateTo({ url: '/pages/admin/index/index' })}
|
||||
>
|
||||
<Text className='text-lg mr-2'>🛡️</Text>
|
||||
<Text className='text-sm font-medium text-purple-700'>平台管理</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
{/* 轮播图:有数据才显示,为空则不渲染 */}
|
||||
{bannerSlides.length > 0 && (
|
||||
<View className='mx-3 mt-2 rounded-lg overflow-hidden index-banner-wrap'>
|
||||
<Swiper
|
||||
autoplay
|
||||
interval={3000}
|
||||
@@ -277,12 +302,8 @@ const IndexPage: React.FC = () => {
|
||||
</SwiperItem>
|
||||
))}
|
||||
</Swiper>
|
||||
) : (
|
||||
<View className='w-full bg-green-50 flex items-center justify-center' style={{ height: `${bannerHeight}px` }}>
|
||||
<Text className='text-gray-400 text-sm'>暂无轮播图</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 功能入口网格 */}
|
||||
<View className='mx-3 mt-3 p-3 bg-white rounded-lg'>
|
||||
|
||||
@@ -50,7 +50,7 @@ const GiftCardPurchasePage: React.FC = () => {
|
||||
<View className='absolute rounded-full' style={{ top: '-20px', right: '-20px', width: '80px', height: '80px', backgroundColor: 'rgba(255,255,255,0.2)' }} />
|
||||
<View className='absolute rounded-full' style={{ bottom: '-32px', left: '-32px', width: '96px', height: '96px', backgroundColor: 'rgba(255,255,255,0.2)' }} />
|
||||
|
||||
<Text className='text-sm opacity-80 block mb-2'>威硕谱软件礼品卡</Text>
|
||||
<Text className='text-sm opacity-80 block mb-2'>威数谱软件礼品卡</Text>
|
||||
<Text className='text-4xl font-bold block mb-3'>
|
||||
¥ {amount || '0'}
|
||||
</Text>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getMyClerk } from '@/api/shop/shopStoreUser'
|
||||
import { listShopDealerApply } from '@/api/shop/shopDealerApply'
|
||||
import { pageShopOrder } from '@/api/shop/shopOrder'
|
||||
import Badge from '@/components/common/Badge'
|
||||
import { useUser } from '@/hooks/useUser'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '门店中心',
|
||||
@@ -45,9 +46,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',
|
||||
@@ -61,30 +62,28 @@ const SUBSCRIBE_TMPL_IDS = [
|
||||
]
|
||||
|
||||
export default function StoreCenterPage() {
|
||||
const { user, isLoggedIn, loading: userLoading } = useUser()
|
||||
const isSuperAdmin = (user as any)?.isSuperAdmin === true
|
||||
const [storeInfo, setStoreInfo] = useState<any>(null)
|
||||
const [pendingVipCount, setPendingVipCount] = useState(0)
|
||||
const [pendingOrderCount, setPendingOrderCount] = useState(0)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
// 验证店员身份
|
||||
getMyClerk()
|
||||
.then(data => {
|
||||
if (!data) {
|
||||
Taro.showToast({ title: '仅门店店员可访问', icon: 'none' })
|
||||
setTimeout(() => Taro.navigateBack(), 1500)
|
||||
return
|
||||
}
|
||||
setStoreInfo(data)
|
||||
// 加载待审核数量
|
||||
loadBadgeCounts()
|
||||
})
|
||||
.catch(() => {
|
||||
Taro.showToast({ title: '仅门店店员可访问', icon: 'none' })
|
||||
// 仅超级管理员可访问
|
||||
if (!isLoggedIn || !isSuperAdmin) {
|
||||
if (isLoggedIn && !isSuperAdmin) {
|
||||
Taro.showToast({ title: '仅超级管理员可访问', icon: 'none' })
|
||||
setTimeout(() => Taro.navigateBack(), 1500)
|
||||
})
|
||||
.finally(() => setLoading(false))
|
||||
}, [])
|
||||
}
|
||||
return
|
||||
}
|
||||
// 超管若同时具备店员身份,加载门店信息用于头部展示(非必须)
|
||||
getMyClerk()
|
||||
.then(data => setStoreInfo(data || null))
|
||||
.catch(() => setStoreInfo(null))
|
||||
// 加载待审核数量
|
||||
loadBadgeCounts()
|
||||
}, [isLoggedIn, isSuperAdmin])
|
||||
|
||||
/** 加载各类待处理角标数量 */
|
||||
const loadBadgeCounts = async () => {
|
||||
@@ -140,7 +139,7 @@ export default function StoreCenterPage() {
|
||||
})
|
||||
}, [])
|
||||
|
||||
if (loading) {
|
||||
if (userLoading) {
|
||||
return (
|
||||
<View className='min-h-full bg-gray-50 flex items-center justify-center'>
|
||||
<Text className='text-gray-400'>加载中...</Text>
|
||||
@@ -148,12 +147,43 @@ export default function StoreCenterPage() {
|
||||
)
|
||||
}
|
||||
|
||||
if (!storeInfo) return null
|
||||
if (!isLoggedIn) {
|
||||
return (
|
||||
<View className='min-h-full bg-gray-50 flex flex-col items-center justify-center py-24'>
|
||||
<Text className='text-5xl mb-3'>🔐</Text>
|
||||
<Text className='text-base text-gray-700 mb-2'>请先登录</Text>
|
||||
<Text className='text-xs text-gray-400 text-center'>登录后方可进入门店中心</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
if (!isSuperAdmin) {
|
||||
return (
|
||||
<View className='min-h-full bg-gray-50 flex flex-col items-center justify-center py-20 px-10'>
|
||||
<Text className='text-5xl mb-3'>🛡️</Text>
|
||||
<Text className='text-base text-gray-700 mb-2'>无访问权限</Text>
|
||||
<Text className='text-xs text-gray-400 text-center'>门店中心仅对超级管理员开放</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
// 头部展示信息:超管同时是店员则展示门店,否则展示管理员身份兜底
|
||||
const header = storeInfo
|
||||
? {
|
||||
avatar: storeInfo.avatar,
|
||||
title: storeInfo.storeName || '门店中心',
|
||||
subtitle: `${storeInfo.name || ''}${storeInfo.phone ? ` · ${storeInfo.phone}` : ''}`,
|
||||
}
|
||||
: {
|
||||
avatar: (user as any)?.avatar,
|
||||
title: '门店中心',
|
||||
subtitle: `${((user as any)?.nickname || (user as any)?.username || '')}${((user as any)?.phone) ? ` · ${(user as any).phone}` : ''}`,
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='min-h-full bg-gray-50'>
|
||||
|
||||
{/* 顶部信息区:店员头像 + 门店名称 */}
|
||||
{/* 顶部信息区:头像 + 名称 */}
|
||||
<View
|
||||
className='pt-8 pb-12 px-5 rounded-b-3xl relative overflow-hidden'
|
||||
style={{ background: 'linear-gradient(135deg, #15803d 0%, #22c55e 60%, #4ade80 100%)' }}
|
||||
@@ -163,12 +193,12 @@ export default function StoreCenterPage() {
|
||||
<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 items-center gap-4'>
|
||||
{/* 店员头像 */}
|
||||
{/* 头像 */}
|
||||
<View className='w-16 h-16 rounded-full bg-white/20 border-2 border-white/40 overflow-hidden flex-shrink-0'>
|
||||
{storeInfo.avatar ? (
|
||||
{header.avatar ? (
|
||||
<Image
|
||||
className='w-full h-full'
|
||||
src={storeInfo.avatar}
|
||||
src={header.avatar}
|
||||
mode='aspectFill'
|
||||
/>
|
||||
) : (
|
||||
@@ -178,13 +208,13 @@ export default function StoreCenterPage() {
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 门店信息 */}
|
||||
{/* 信息 */}
|
||||
<View className='flex-1 min-w-0'>
|
||||
<Text className='text-white text-lg font-bold block truncate'>
|
||||
{storeInfo.storeName || '门店中心'}
|
||||
{header.title}
|
||||
</Text>
|
||||
<Text className='text-white text-opacity-80 text-sm mt-1 block truncate'>
|
||||
{storeInfo.name} {storeInfo.phone ? `· ${storeInfo.phone}` : ''}
|
||||
{header.subtitle}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -164,30 +164,6 @@ const ProfilePage: React.FC = () => {
|
||||
<Text className='text-sm text-gray-600'>{registerTime || '未知'}</Text>
|
||||
</View>
|
||||
|
||||
{/* 门店名称 */}
|
||||
<View className='flex items-center justify-between px-4 py-3 bg-white border-b border-gray-50'>
|
||||
<Text className='text-sm text-gray-700 w-20'>门店名称</Text>
|
||||
<Input
|
||||
className='flex-1 text-right text-sm text-gray-600'
|
||||
placeholder='请输入门店名称'
|
||||
value={(form as any)?.merchantName || ''}
|
||||
onInput={(e: any) => setForm(prev => ({ ...prev, merchantName: e.detail.value }))}
|
||||
maxlength={50}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 门店地址 */}
|
||||
<View className='flex items-center justify-between px-4 py-3 bg-white border-b border-gray-50'>
|
||||
<Text className='text-sm text-gray-700 w-20'>门店地址</Text>
|
||||
<Input
|
||||
className='flex-1 text-right text-sm text-gray-600'
|
||||
placeholder='请输入门店地址'
|
||||
value={(form as any)?.address || ''}
|
||||
onInput={(e: any) => setForm(prev => ({ ...prev, address: e.detail.value }))}
|
||||
maxlength={100}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 保存按钮 */}
|
||||
<BottomButton
|
||||
text='保存'
|
||||
|
||||
@@ -101,10 +101,14 @@ const UserPage: React.FC = () => {
|
||||
})
|
||||
|
||||
const menuItems = [
|
||||
// 升级VIP会员 - 醒目样式
|
||||
{ icon: '👑', label: '升级VIP会员', 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 }] : []),
|
||||
// 申请成为开发者 / 进入开发者中心:已通过审核则直接进开发者中心
|
||||
...((user as any)?.isDeveloper
|
||||
? [{ icon: '🛠️', label: '进入开发者中心', url: '/pages/developer/index/index', highlight: true, requireAuth: true }]
|
||||
: [{ icon: '🔰', label: '申请成为开发者', url: '/pages/user/vip-upgrade/index', highlight: true, requireAuth: true }]),
|
||||
// 平台管理:仅 isSuperAdmin=true 用户显示
|
||||
...((user as any)?.isSuperAdmin === true ? [{ icon: '🛡️', label: '平台管理', url: '/pages/admin/index/index', requireAuth: true }] : []),
|
||||
// 门店中心:仅 isSuperAdmin=true 用户显示
|
||||
...((user as any)?.isSuperAdmin === true ? [{ icon: '🏪', label: '门店中心', url: '/pages/store/center/index', requireAuth: true }] : []),
|
||||
{ icon: '💰', label: '我的钱包', url: '/pages/user/wallet', requireAuth: true },
|
||||
{ icon: '📍', label: '收货地址', url: '/pages/user/address-list', requireAuth: true },
|
||||
// { icon: '⭐', label: '积分明细', url: '/pages/user/points-record' },
|
||||
|
||||
@@ -3,11 +3,11 @@ import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import Taro, { useDidShow } from '@tarojs/taro'
|
||||
import { listShopDealerApply, updateShopDealerApply } from '@/api/shop/shopDealerApply'
|
||||
import type { ShopDealerApply } from '@/api/shop/shopDealerApply/model'
|
||||
import { addUserRole, listUserRole } from '@/api/system/userRole'
|
||||
import { getMyClerk } from '@/api/shop/shopStoreUser'
|
||||
import { setUserDeveloper } from '@/api/system/user'
|
||||
import { getMyClerk, addShopStoreUser } from '@/api/shop/shopStoreUser'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: 'VIP会员审核',
|
||||
navigationBarTitleText: '开发者审核',
|
||||
})
|
||||
|
||||
// Tab 配置
|
||||
@@ -19,11 +19,6 @@ const TABS: { key: TabKey; label: string; status: number }[] = [
|
||||
{ key: 'rejected', label: '已驳回', status: 30 },
|
||||
]
|
||||
|
||||
// VIP 角色信息(对应 system/role 表)
|
||||
const VIP_ROLE_ID = 2032
|
||||
const VIP_ROLE_CODE = 'vip'
|
||||
const VIP_ROLE_NAME = 'VIP会员'
|
||||
|
||||
// 格式化时间戳
|
||||
function formatTime(time?: number | string): string {
|
||||
if (!time) return '-'
|
||||
@@ -43,50 +38,14 @@ function formatTime(time?: number | string): string {
|
||||
return `${y}-${m}-${d} ${h}:${mi}`
|
||||
}
|
||||
|
||||
// 格式化日期为 Spring Boot LocalDateTime 格式
|
||||
function formatDateTime(date: Date): string {
|
||||
const y = date.getFullYear()
|
||||
const m = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const d = String(date.getDate()).padStart(2, '0')
|
||||
const h = String(date.getHours()).padStart(2, '0')
|
||||
const mi = String(date.getMinutes()).padStart(2, '0')
|
||||
const s = String(date.getSeconds()).padStart(2, '0')
|
||||
return `${y}-${m}-${d} ${h}:${mi}:${s}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 给用户添加 VIP 角色
|
||||
* 先查询是否已有 VIP 角色,没有则新增,避免重复绑定
|
||||
*/
|
||||
async function assignVipRole(userId?: number): Promise<void> {
|
||||
if (!userId) return
|
||||
|
||||
// 查询用户已有角色
|
||||
const userRoles = await listUserRole({ userId })
|
||||
const hasVip = (userRoles || []).some(
|
||||
r => r.roleId === VIP_ROLE_ID || r.roleCode === VIP_ROLE_CODE
|
||||
)
|
||||
|
||||
if (hasVip) {
|
||||
// 已拥有 VIP 角色,跳过
|
||||
return
|
||||
}
|
||||
|
||||
// 新增 VIP 角色绑定
|
||||
await addUserRole({
|
||||
userId,
|
||||
roleId: VIP_ROLE_ID,
|
||||
roleCode: VIP_ROLE_CODE,
|
||||
roleName: VIP_ROLE_NAME,
|
||||
})
|
||||
}
|
||||
|
||||
const VipReviewPage: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState<TabKey>('pending')
|
||||
const [list, setList] = useState<ShopDealerApply[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [isClerk, setIsClerk] = useState(false)
|
||||
const [clerkStoreId, setClerkStoreId] = useState<number>()
|
||||
const [clerkTenantId, setClerkTenantId] = useState<number>()
|
||||
const loadingRef = useRef(false)
|
||||
|
||||
// 验证当前用户是否为店员
|
||||
@@ -95,6 +54,9 @@ const VipReviewPage: React.FC = () => {
|
||||
.then(data => {
|
||||
if (data) {
|
||||
setIsClerk(true)
|
||||
// 记录当前店员所属门店与租户,审核通过时用于给申请人添加店员记录
|
||||
setClerkStoreId(data.storeId)
|
||||
setClerkTenantId(data.tenantId)
|
||||
} else {
|
||||
setIsClerk(false)
|
||||
Taro.showToast({ title: '仅门店店员可访问', icon: 'none' })
|
||||
@@ -155,7 +117,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
|
||||
@@ -172,8 +134,19 @@ const VipReviewPage: React.FC = () => {
|
||||
auditTime: Date.now(),
|
||||
} as ShopDealerApply)
|
||||
|
||||
// 审核通过后,给用户添加 VIP 角色
|
||||
await assignVipRole(item.userId)
|
||||
// 审核通过:升级为开发者(后端 markAsDeveloper 会置 is_developer=1 并把角色替换为 developer)
|
||||
await setUserDeveloper(item.userId!)
|
||||
|
||||
// 审核通过:在当前门店下给申请人添加一条店员(shop_store_user)记录
|
||||
if (clerkStoreId) {
|
||||
await addShopStoreUser({
|
||||
userId: item.userId!,
|
||||
storeId: clerkStoreId,
|
||||
tenantId: clerkTenantId,
|
||||
isDelete: 0,
|
||||
comments: '开发者审核通过自动添加',
|
||||
})
|
||||
}
|
||||
|
||||
Taro.showToast({ title: '已通过', icon: 'success' })
|
||||
loadList(activeTab)
|
||||
@@ -190,7 +163,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
|
||||
@@ -265,12 +238,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>
|
||||
@@ -279,7 +252,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>
|
||||
@@ -345,7 +318,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>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export default {
|
||||
navigationBarTitleText: '升级VIP会员',
|
||||
navigationBarTitleText: '申请成为开发者',
|
||||
enableShareAppMessage: true,
|
||||
enableShareTimeline: true,
|
||||
}
|
||||
|
||||
@@ -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'>
|
||||
|
||||
@@ -11,7 +11,7 @@ const TERMS_CONTENT = `
|
||||
<p><strong>最后更新日期:2026年5月16日</strong></p>
|
||||
|
||||
<h3>一、协议的范围与接受</h3>
|
||||
<p>欢迎使用威硕谱软件(以下简称"本小程序")。本小程序由南宁市网宿信息科技有限公司(以下简称"我们")开发并运营。请您在使用本小程序服务之前,仔细阅读并充分理解本协议的全部内容。您点击"同意"或实际使用本小程序服务,即视为您已阅读、理解并同意接受本协议的约束。</p>
|
||||
<p>欢迎使用威数谱软件(以下简称"本小程序")。本小程序由南宁市网宿信息科技有限公司(以下简称"我们")开发并运营。请您在使用本小程序服务之前,仔细阅读并充分理解本协议的全部内容。您点击"同意"或实际使用本小程序服务,即视为您已阅读、理解并同意接受本协议的约束。</p>
|
||||
|
||||
<h3>二、服务内容</h3>
|
||||
<p>本小程序为用户提供以下服务:</p>
|
||||
|
||||
@@ -293,7 +293,7 @@ const Login = () => {
|
||||
<View className='login-content'>
|
||||
{/* Logo 和标题 */}
|
||||
<View className='login-header'>
|
||||
<Text className='login-title'>欢迎来到威硕谱软件</Text>
|
||||
<Text className='login-title'>欢迎来到威数谱软件</Text>
|
||||
<Text className='login-subtitle'>登录后享受更多精彩功能</Text>
|
||||
</View>
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { Button, Cell, Price, Divider } from '@nutui/nutui-react-taro'
|
||||
import { Check, Close, Tips, Clock } from '@nutui/icons-react-taro'
|
||||
import { View, Text, Button } from '@tarojs/components'
|
||||
import { Tips, Check, Close, Clock } from '@nutui/icons-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { request } from '@/utils/request'
|
||||
import { getOpenId, loginByOpenId } from '@/api/passport/wx-login'
|
||||
import type { User } from '@/api/system/user/model'
|
||||
import { saveStorageByLoginUser } from '@/utils/server'
|
||||
import { TenantId, WebsopyBaseUrl } from '@/config/app'
|
||||
import Price from '@/components/common/Price'
|
||||
|
||||
/**
|
||||
* 小程序支付页面
|
||||
@@ -254,12 +254,9 @@ const PayPage: React.FC = () => {
|
||||
setPaid(true)
|
||||
Taro.showToast({ title: '支付成功', icon: 'success' })
|
||||
|
||||
// 8. 延迟自动跳转到已购产品页面
|
||||
// 8. 延迟自动跳转到“我的”页面(pages/user/user 为 tabBar 页,用 switchTab)
|
||||
setTimeout(() => {
|
||||
Taro.navigateTo({ url: '/user/apps/index' }).catch(() => {
|
||||
// 如果 navigateTo 失败(比如在 tab 页),回退到用户页
|
||||
Taro.switchTab({ url: '/pages/user/user' })
|
||||
})
|
||||
Taro.switchTab({ url: '/pages/user/user' })
|
||||
}, 1500)
|
||||
|
||||
} catch (err: any) {
|
||||
@@ -315,7 +312,12 @@ const PayPage: React.FC = () => {
|
||||
<Close className="text-red-500 mb-4" size="48" />
|
||||
<Text className="block text-gray-800 text-lg mb-2">获取订单失败</Text>
|
||||
<Text className="block text-gray-500 mb-6">{errorMsg}</Text>
|
||||
<Button type="primary" onClick={handleBack}>返回</Button>
|
||||
<Button
|
||||
onClick={handleBack}
|
||||
className="bg-blue-500 text-white rounded-full px-6"
|
||||
>
|
||||
返回
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
@@ -336,7 +338,12 @@ const PayPage: React.FC = () => {
|
||||
已支付 ¥{Number(detail.payPrice).toFixed(2)}
|
||||
</Text>
|
||||
) : null}
|
||||
<Button type="primary" onClick={handleBack}>完成</Button>
|
||||
<Button
|
||||
onClick={handleBack}
|
||||
className="bg-blue-500 text-white rounded-full px-6"
|
||||
>
|
||||
完成
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
@@ -350,21 +357,26 @@ const PayPage: React.FC = () => {
|
||||
<View className="bg-white rounded-lg shadow-sm p-4 mb-4">
|
||||
<Text className="block text-lg font-bold text-gray-800 mb-3">确认支付</Text>
|
||||
|
||||
<Cell title="商品名称" description={detail?.productName || '-'} />
|
||||
<Cell title="价格类型" description={getPriceDesc()} />
|
||||
|
||||
<View className="flex items-center justify-between px-4 py-3">
|
||||
<Text className="text-gray-600">支付金额</Text>
|
||||
<Price
|
||||
price={detail?.payPrice}
|
||||
size="large"
|
||||
thousands
|
||||
/>
|
||||
{/* 商品名称 */}
|
||||
<View className="flex items-center justify-between px-4 py-3 border-b border-gray-100">
|
||||
<Text className="text-gray-600">商品名称</Text>
|
||||
<Text className="text-gray-800">{detail?.productName || '-'}</Text>
|
||||
</View>
|
||||
|
||||
<Divider />
|
||||
{/* 价格类型 */}
|
||||
<View className="flex items-center justify-between px-4 py-3 border-b border-gray-100">
|
||||
<Text className="text-gray-600">价格类型</Text>
|
||||
<Text className="text-gray-800">{getPriceDesc() || '-'}</Text>
|
||||
</View>
|
||||
|
||||
<View className="flex items-start px-4 py-2">
|
||||
{/* 支付金额 */}
|
||||
<View className="flex items-center justify-between px-4 py-3">
|
||||
<Text className="text-gray-600">支付金额</Text>
|
||||
<Price price={detail?.payPrice} size="large" />
|
||||
</View>
|
||||
|
||||
{/* 提示 */}
|
||||
<View className="flex items-start px-4 py-2 mt-2 border-t border-gray-100">
|
||||
<Tips className="text-orange-500 mr-2 mt-1" size="16" />
|
||||
<Text className="text-sm text-gray-500">
|
||||
支付成功后即可在「已购产品」中使用该应用
|
||||
@@ -376,18 +388,15 @@ const PayPage: React.FC = () => {
|
||||
<View className="fixed bottom-0 left-0 right-0 p-4 bg-white border-t border-gray-100">
|
||||
<View className="flex gap-3">
|
||||
<Button
|
||||
block
|
||||
onClick={handleBack}
|
||||
className="flex-1"
|
||||
className="flex-1 bg-gray-100 text-gray-700 rounded-full border-0"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
block
|
||||
loading={paying}
|
||||
onClick={handlePay}
|
||||
className="flex-1"
|
||||
className="flex-1 bg-blue-500 text-white rounded-full border-0"
|
||||
>
|
||||
{paying ? '支付中...' : '立即支付'}
|
||||
</Button>
|
||||
|
||||
138
src/utils/devcenter.ts
Normal file
138
src/utils/devcenter.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
// 开发者中心 / 平台管理 共用工具:分页提取、枚举中文、脱敏
|
||||
|
||||
/** 从后端分页结果中提取列表(兼容 list / records / 纯数组) */
|
||||
export function getPageList(data: any): any[] {
|
||||
if (!data) return []
|
||||
if (Array.isArray(data.list)) return data.list
|
||||
if (Array.isArray(data.records)) return data.records
|
||||
if (Array.isArray(data)) return data
|
||||
return []
|
||||
}
|
||||
|
||||
/** 从后端分页结果中提取总数 */
|
||||
export function getPageTotal(data: any): number {
|
||||
if (!data) return 0
|
||||
return data.total ?? data.count ?? getPageList(data).length
|
||||
}
|
||||
|
||||
export const APP_TYPE_NAME: Record<number, string> = {
|
||||
10: '网站',
|
||||
20: '微信小程序',
|
||||
30: '抖音小程序',
|
||||
40: '百度小程序',
|
||||
50: '支付宝小程序',
|
||||
60: 'Android',
|
||||
70: 'iOS',
|
||||
80: 'macOS',
|
||||
90: 'Windows',
|
||||
100: '插件',
|
||||
110: '平台应用',
|
||||
}
|
||||
|
||||
export const PUBLISH_STATUS_NAME: Record<string, string> = {
|
||||
developing: '开发中',
|
||||
pending_review: '待审核',
|
||||
published: '已上架',
|
||||
rejected: '未通过',
|
||||
deprecated: '已下架',
|
||||
}
|
||||
|
||||
export const STATUS_NAME: Record<number, string> = {
|
||||
0: '未开通',
|
||||
1: '运行中',
|
||||
2: '维护中',
|
||||
3: '已关闭',
|
||||
4: '已欠费',
|
||||
5: '违规停机',
|
||||
}
|
||||
|
||||
export const DOMAIN_STATUS_NAME: Record<number, string> = {
|
||||
0: '待解析',
|
||||
1: '校验中',
|
||||
2: '已生效',
|
||||
3: '已拒绝',
|
||||
4: '已停用',
|
||||
}
|
||||
|
||||
export const TICKET_STATUS_NAME: Record<string, string> = {
|
||||
pending: '待处理',
|
||||
assigned: '已分配',
|
||||
processing: '处理中',
|
||||
resolved: '已解决',
|
||||
closed: '已关闭',
|
||||
rejected: '已拒绝',
|
||||
}
|
||||
|
||||
export const TICKET_PRIORITY_NAME: Record<string, string> = {
|
||||
urgent: '紧急',
|
||||
high: '高',
|
||||
normal: '普通',
|
||||
low: '低',
|
||||
}
|
||||
|
||||
export const TICKET_CATEGORY_NAME: Record<string, string> = {
|
||||
bug: '缺陷',
|
||||
feature: '需求',
|
||||
consultation: '咨询',
|
||||
complaint: '投诉',
|
||||
other: '其他',
|
||||
}
|
||||
|
||||
/** API Key 脱敏:保留前缀与后4位 */
|
||||
export function maskKey(key?: string): string {
|
||||
if (!key) return ''
|
||||
if (key.length <= 12) return key
|
||||
return key.slice(0, 8) + '••••••••' + key.slice(-4)
|
||||
}
|
||||
|
||||
/** 颜色映射:根据状态返回主题色 */
|
||||
export const STATUS_COLORS: Record<string, string> = {
|
||||
green: '#16a34a',
|
||||
red: '#dc2626',
|
||||
amber: '#d97706',
|
||||
blue: '#2563eb',
|
||||
gray: '#6b7280',
|
||||
}
|
||||
|
||||
export function publishStatusColor(status?: string): string {
|
||||
switch (status) {
|
||||
case 'published':
|
||||
return STATUS_COLORS.green
|
||||
case 'pending_review':
|
||||
return STATUS_COLORS.amber
|
||||
case 'rejected':
|
||||
return STATUS_COLORS.red
|
||||
default:
|
||||
return STATUS_COLORS.gray
|
||||
}
|
||||
}
|
||||
|
||||
export function domainStatusColor(status?: number): string {
|
||||
switch (status) {
|
||||
case 2:
|
||||
return STATUS_COLORS.green
|
||||
case 0:
|
||||
case 1:
|
||||
return STATUS_COLORS.amber
|
||||
case 3:
|
||||
case 4:
|
||||
return STATUS_COLORS.red
|
||||
default:
|
||||
return STATUS_COLORS.gray
|
||||
}
|
||||
}
|
||||
|
||||
export function ticketStatusColor(status?: string): string {
|
||||
switch (status) {
|
||||
case 'resolved':
|
||||
case 'closed':
|
||||
return STATUS_COLORS.green
|
||||
case 'pending':
|
||||
return STATUS_COLORS.red
|
||||
case 'processing':
|
||||
case 'assigned':
|
||||
return STATUS_COLORS.blue
|
||||
default:
|
||||
return STATUS_COLORS.gray
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user