2 Commits

Author SHA1 Message Date
gxwebsoft 85a1558e84 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 依赖库问题不影响正常构建和运行
2026-07-16 01:34:27 +08:00
gxwebsoft 9df90c1176 feat(address): 新增收货地址与售后申请功能
- 新增地址编辑页面,支持地址智能识别、地图选点、地区选择和默认地址设置
- 实现地址列表页面,支持地址查看、删除、设为默认及选择返回结算页
- 新增售后申请页面,支持退款类型选择、商品选择、原因填写、图片上传和提交审核
- 修复 passport 分包配置,移除不存在分包并补充缺失声明,避免 Taro 编译报错
- 新增地址类型定义,增强前端地址数据结构类型安全
- 优化页面交互体验,完善表单校验及错误提示逻辑
- 统一代码格式与命名规范,保持代码风格一致性
2026-07-16 01:18:42 +08:00
983 changed files with 51455 additions and 73591 deletions
-12
View File
@@ -1,12 +0,0 @@
# http://editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
-7
View File
@@ -1,7 +0,0 @@
module.exports = {
"extends": ["taro/react"],
"rules": {
"react/jsx-uses-react": "off",
"react/react-in-jsx-scope": "off"
}
}
-25
View File
@@ -1,25 +0,0 @@
dist/
deploy_versions/
.temp/
.rn_temp/
node_modules/
.DS_Store
.swc
/.idea/
# 环境变量文件
.env
.env.local
.env.development
.env.production
# 日志文件
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# 编辑器配置
.vscode/
*.swp
*.swo
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
+562
View File
@@ -0,0 +1,562 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="2de94e4f-0587-4c9b-8bfd-e87e0ae95b81" name="更改" comment="feat(address): 新增收货地址与售后申请功能&#10;&#10;- 新增地址编辑页面,支持地址智能识别、地图选点、地区选择和默认地址设置&#10;- 实现地址列表页面,支持地址查看、删除、设为默认及选择返回结算页&#10;- 新增售后申请页面,支持退款类型选择、商品选择、原因填写、图片上传和提交审核&#10;- 修复 passport 分包配置,移除不存在分包并补充缺失声明,避免 Taro 编译报错&#10;- 新增地址类型定义,增强前端地址数据结构类型安全&#10;- 优化页面交互体验,完善表单校验及错误提示逻辑&#10;- 统一代码格式与命名规范,保持代码风格一致性">
<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>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
<option name="RESET_MODE" value="HARD" />
</component>
<component name="ProjectColorInfo">{
&quot;associatedIndex&quot;: 7
}</component>
<component name="ProjectId" id="3CCElusNNIyc7lgX61jkNQo02h3" />
<component name="ProjectLevelVcsManager">
<ConfirmationsSetting value="2" id="Add" />
</component>
<component name="ProjectViewState">
<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>
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/src/passport" />
<recent name="$PROJECT_DIR$/src/api" />
<recent name="$PROJECT_DIR$/src/api/passport/wx-login" />
<recent name="$PROJECT_DIR$" />
</key>
</component>
<component name="RunManager" selected="npm.build:weapp">
<configuration name="build:weapp" type="js.build_tools.npm" temporary="true" nameIsGenerated="true">
<package-json value="$PROJECT_DIR$/package.json" />
<command value="run" />
<scripts>
<script value="build:weapp" />
</scripts>
<node-interpreter value="project" />
<envs />
<method v="2" />
</configuration>
<configuration name="dev:weapp" type="js.build_tools.npm" temporary="true" nameIsGenerated="true">
<package-json value="$PROJECT_DIR$/package.json" />
<command value="run" />
<scripts>
<script value="dev:weapp" />
</scripts>
<node-interpreter value="project" />
<envs />
<method v="2" />
</configuration>
<recent_temporary>
<list>
<item itemvalue="npm.build:weapp" />
<item itemvalue="npm.dev:weapp" />
</list>
</recent_temporary>
</component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-js-predefined-d6986cc7102b-e03c56caf84a-JavaScript-WS-252.23892.411" />
</set>
</attachedChunks>
</component>
<component name="TaskManager">
<task active="true" id="Default" summary="默认任务">
<changelist id="2de94e4f-0587-4c9b-8bfd-e87e0ae95b81" name="更改" comment="" />
<created>1775881305279</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1775881305279</updated>
<workItem from="1775881306434" duration="12652000" />
<workItem from="1775910653750" duration="20840000" />
<workItem from="1777873063244" duration="20000" />
<workItem from="1778089953570" duration="7000" />
<workItem from="1778308774420" duration="947000" />
<workItem from="1780559793276" duration="400000" />
<workItem from="1782803615391" duration="418000" />
<workItem from="1782804285999" duration="4025000" />
<workItem from="1782813891753" duration="1850000" />
<workItem from="1782876148090" duration="4787000" />
<workItem from="1782903749109" duration="4000" />
<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路径&#10;&#10;- 将邀请接受接口URL从/api/_app/invite/accept修改为/api/_app/developer/invite/accept&#10;- 确保请求正确指向开发者邀请接受接口&#10;- 修复因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): 优化登录确认成功后的页面跳转逻辑&#10;&#10;- 延长登录确认成功提示的显示时间至2000毫秒&#10;- 跳转到用户页面前先隐藏toast,避免影响跳转体验&#10;- 增加switchTab成功和失败的日志打印&#10;- 失败时提示用户手动返回页面&#10;- 调整跳转延迟时间为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): 修正邀请信息接口请求参数与请求头&#10;&#10;- 将邀请令牌作为查询参数附加在请求URL中&#10;- 移除请求体中的token字段传递&#10;- 添加请求头TenantId,值为5&#10;- 请求失败时在控制台打印错误信息以便调试&#10;- 保持原有错误处理和加载状态设置逻辑不变">
<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传递问题&#10;&#10;- 从配置中引入TenantId常量&#10;- 将请求header中的TenantId字段由硬编码改为动态变量&#10;- 确保GET请求和加密数据请求均正确带上TenantId头信息&#10;- 提升接口调用的一致性和维护性">
<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地址&#10;&#10;- 将开发环境、生产环境和测试环境的API_BASE_URL统一更新为新的域名websopy-api.websoft.top&#10;- 更新SimpleQRCodeModal组件中二维码请求地址,使用新的API域名&#10;- 在邀请信息请求函数fetchInviteInfo中添加详细的请求和响应日志&#10;- 增加接口错误日志打印,提高捕获异常时的信息丰富度">
<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域名&#10;&#10;- 将邀请相关接口的请求地址由通用API域名改为独立的INVITE_API_URL&#10;- 修改获取邀请信息接口的请求URL&#10;- 修改接受邀请接口的请求URL&#10;- 确保请求头和参数传递保持一致">
<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&#10;&#10;- 从cmsWebsiteField接口中删除configWebsiteField方法&#10;- 删除useConfig自定义Hook及其引用&#10;- 更新about页面,去除对config对象和useConfig的使用&#10;- 修正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&#10;&#10;- 从cmsWebsiteField接口中删除configWebsiteField方法&#10;- 删除useConfig自定义Hook及其引用&#10;- 更新about页面,去除对config对象和useConfig的使用&#10;- 修正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): 修正邀请接口路径以支持小程序专用前缀&#10;&#10;- 将请求邀请信息接口路径改为使用 /api/_app 前缀,确保小程序免登录访问&#10;- 修改接受邀请接口路径,统一使用小程序专用 API 域名和前缀&#10;- 更新邀请相关接口注释,明确小程序专用接口前缀含义&#10;- 在专家历史数据中添加高级开发工程师 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): 添加邀请接受接口调试日志&#10;&#10;- 在接受邀请请求前打印token和请求URL、参数&#10;- 在请求响应后输出接口响应及返回数据&#10;- 在调用失败时打印错误信息及异常日志&#10;- 保持请求流程和状态管理不变,增强调试能力">
<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): 添加邀请接受接口调试日志&#10;&#10;- 在接受邀请请求前打印token和请求URL、参数&#10;- 在请求响应后输出接口响应及返回数据&#10;- 在调用失败时打印错误信息及异常日志&#10;- 保持请求流程和状态管理不变,增强调试能力">
<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): 优化邀请码接受失败的用户提示和处理&#10;&#10;- 针对“用户不存在”、“用户创建失败”及“请先登录”提示,改为弹窗提示需登录&#10;- 在弹窗中增加“去登录”按钮,点击后跳转到登录页面,并携带重定向token参数&#10;- 其他错误仍使用原Toast提示方式展示错误信息&#10;- 保留错误日志打印,便于排查异常情况">
<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): 优化新用户扫码邀请加入流程&#10;&#10;- 扫码进入邀请页面时保存邀请 token 到本地存储&#10;- 新增登录状态检测,未登录用户点击快速加入时保存邀请信息并跳转登录页&#10;- 登录成功后返回邀请页自动执行加入应用操作,提升用户体验&#10;- 支持已登录用户直接加入应用,简化流程&#10;- 增加清理 pending 邀请数据的方法,防止数据残留&#10;- 监听页面显示事件,处理登录返回后自动操作&#10;- 调整接口请求逻辑,确保携带最新的邀请和授权数据&#10;- 更新项目配置,注册邀请页面路径信息">
<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): 重写登录与邀请加入流程,支持微信手机号一键登录&#10;&#10;- 完全重写 passport/login.tsx,实现微信手机号一键登录功能&#10;- 支持登录页面携带 redirect 参数,登录后自动跳转回原页面&#10;- 登录成功后自动处理邀请加入应用,支持邀请关系绑定&#10;- 重写 passport/invite/index.tsx,实现扫码邀请加入应用完整流程&#10;- 支持调用 wx.login 获取 code,使用 loginByOpenId 判断注册状态&#10;- 未注册用户自动跳转登录页完成微信手机号登录再加入应用&#10;- 已登录用户直接执行加入应用操作,加入接口支持 Authorization 头&#10;- 解决邀请页面与登录页面间旧授权码混用导致加入失败问题&#10;- 处理后端 401 错误,优先使用微信授权码免登录接口&#10;- 优化用户体验,添加服务协议与隐私政策弹窗及错误提示&#10;- 页面状态管理改进,加入加载、检查登录、错误和跳转反馈">
<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): 重构邀请加入流程,实现已登录用户免授权直接加入&#10;&#10;- 完全重写邀请页逻辑,区分已登录与未注册用户按钮显示&#10;- 已登录用户显示“确认加入”按钮,点击用 wx.login 获取 code 加入&#10;- 未注册用户显示“微信手机号快速加入”按钮,授权手机号后完成注册登录再加入&#10;- 移除跳转登录页面逻辑,所有流程在邀请页内完成&#10;- 加入接口统一使用 Authorization 头携带 token,确保身份双重验证&#10;- 优化背景、粒子和扫描线效果,提升界面视觉体验&#10;- 增加错误处理提示,加入失败时显示详细原因并可返回首页&#10;- 协议勾选仅在未注册用户显示,确保合规授权流程">
<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): 优化已登录用户加入时协议勾选逻辑&#10;&#10;- 已登录用户点击「确认加入」时不再强制勾选协议&#10;- 移除 handleConfirmJoin 中对协议勾选的检查&#10;- 未注册用户仍需勾选协议后才能授权手机号&#10;- 确保登录状态异常时有提示信息显示">
<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): 支持已登录用户直接加入应用&#10;&#10;- 后端accept接口code参数改为可选,支持通过Authorization头识别登录用户&#10;- 已登录用户加入时不再弹手机号授权,前端调用新增的doJoinAppForLoggedInUser方法&#10;- 未注册用户仍通过手机号授权码code加入,调用doJoinAppForNewUser方法&#10;- 拆分前端加入逻辑,新增handleConfirmJoin统一处理已登录加入流程&#10;- 优化按钮交互,已登录状态下按钮变为普通点击,移除协议强制勾选&#10;- 错误处理和状态提示完善,加入成功后自动跳转首页">
<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): 完成小程序开发者中心和企业控制台改造&#10;&#10;- 设计并实现了开发者中心与企业控制台两大模块&#10;- 按用户角色区分开发者和企业客户,支持多项目类型及成员管理&#10;- 新增项目管理、应用管理、API Key管理及成员邀请等多功能页面&#10;- 实现应用版本发布、消息通知中心、权限审批与开发者申请流程&#10;- 完成CI/CD流水线、运营监控、发票管理、SSO单点登录功能&#10;- 搭建SDK下载中心、工单系统、FAQ系统、数据导入导出等模块&#10;- 优化后端API,支持已登录和未注册用户不同加入应用流程&#10;- 前端按钮统一采用微信手机号授权,完善用户授权体验&#10;- 修复多个页面的JSX语法错误及依赖导入问题,替换部分组件库&#10;- 增加详细的类型定义文件,提升项目类型安全&#10;- 新增超过55个页面及60个API接口,扩展应用功能和服务体系&#10;- 完成全面的样式设计,实现一致的视觉风格和交互体验">
<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): 优化隐私协议同意逻辑及样式&#10;&#10;- 修正api-keys样式导入路径,改为developer路径&#10;- 在开发者申请页面增加服务协议和隐私政策勾选框&#10;- 提交时校验协议是否已勾选,未勾选时提示用户&#10;- 优化申请页面协议区域样式及交互&#10;- 在用户验证页面添加隐私政策同意勾选框&#10;- 提交用户验证表单时检查协议同意状态&#10;- 增加打开服务协议和隐私政策的链接跳转&#10;- 更新项目配置,增加passport/webview路由配置&#10;- 修复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): 优化隐私协议同意逻辑及样式&#10;&#10;- 修正api-keys样式导入路径,改为developer路径&#10;- 在开发者申请页面增加服务协议和隐私政策勾选框&#10;- 提交时校验协议是否已勾选,未勾选时提示用户&#10;- 优化申请页面协议区域样式及交互&#10;- 在用户验证页面添加隐私政策同意勾选框&#10;- 提交用户验证表单时检查协议同意状态&#10;- 增加打开服务协议和隐私政策的链接跳转&#10;- 更新项目配置,增加passport/webview路由配置&#10;- 修复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>
</task>
<task id="LOCAL-00026" summary="refactor(config): 移除开发者中心和企业控制台的自定义导航样式&#10;&#10;- 删除了开发者中心页面配置中的 navigationStyle: 'custom'&#10;- 删除了企业控制台页面配置中的 navigationStyle: 'custom'&#10;- 简化了页面配置,保持默认导航样式&#10;- 有助于提升页面兼容性和维护性">
<option name="closed" value="true" />
<created>1776019857647</created>
<option name="number" value="00026" />
<option name="presentableId" value="LOCAL-00026" />
<option name="project" value="LOCAL" />
<updated>1776019857647</updated>
</task>
<task id="LOCAL-00027" summary="feat(userVerify): 添加阿里云身份证二要素核验功能&#10;&#10;- 在接口层新增 verifyIdCard 接口,实现姓名与身份证号核验&#10;- userVerify 页面提交时增加身份核验逻辑,核验失败阻止提交&#10;- 调用核验接口时显示加载状态,并根据结果提示用户&#10;- 后端依赖 aliyun cloudauth,提供实人认证相关服务接口&#10;- 完成基本的错误捕获与用户提示,提升实名认证流程安全性">
<option name="closed" value="true" />
<created>1776021606260</created>
<option name="number" value="00027" />
<option name="presentableId" value="LOCAL-00027" />
<option name="project" value="LOCAL" />
<updated>1776021606260</updated>
</task>
<task id="LOCAL-00028" summary="feat(userVerify): 添加阿里云身份证二要素核验功能&#10;&#10;- 在接口层新增 verifyIdCard 接口,实现姓名与身份证号核验&#10;- userVerify 页面提交时增加身份核验逻辑,核验失败阻止提交&#10;- 调用核验接口时显示加载状态,并根据结果提示用户&#10;- 后端依赖 aliyun cloudauth,提供实人认证相关服务接口&#10;- 完成基本的错误捕获与用户提示,提升实名认证流程安全性">
<option name="closed" value="true" />
<created>1776021764419</created>
<option name="number" value="00028" />
<option name="presentableId" value="LOCAL-00028" />
<option name="project" value="LOCAL" />
<updated>1776021764419</updated>
</task>
<task id="LOCAL-00029" summary="refactor(api): 规范前端API接口地址并统一路径前缀&#10;&#10;- 修正开发者、小程序和企业相关API的基础URL,避免重复添加/api前缀&#10;- 统一调整developer、enterprise、invite等模块接口路径,保持与后端Controller一致&#10;- 新增权限申请及消息通知相关API,并完善相关函数实现&#10;- 规范请求参数传递,移除多余包装,改用直接传递对象方式&#10;- 新增API Key重置、版本发布/回滚等接口支持&#10;&#10;feat(invite): 重构邀请登录流程支持未注册快速加入&#10;&#10;- 完整重写invite页,分离已注册用户和未注册用户的按钮显示和交互&#10;- 实现未注册用户微信手机号授权后自动注册登录及自动加入应用&#10;- 已注册用户直接确认加入应用,无需手机号授权弹窗&#10;- 统一使用getPhoneNumber按钮处理两种状态并修复“授权码不能为空”报错&#10;- 前端配合后端改造,支持已登录用户可直接通过Authorization头加入应用&#10;&#10;fix(developer): 修复开发者中心应用加载问题&#10;&#10;- 修复只查询创建的应用导致邀请加入的应用不显示的问题&#10;- 同时请求创建应用和参与应用接口,合并并去重应用列表&#10;- 新增pageJoinedApp接口调用及合并逻辑,提高应用列表完整性和体验">
<option name="closed" value="true" />
<created>1776065433562</created>
<option name="number" value="00029" />
<option name="presentableId" value="LOCAL-00029" />
<option name="project" value="LOCAL" />
<updated>1776065433562</updated>
</task>
<task id="LOCAL-00030" summary="refactor(api): 规范前端API接口地址并统一路径前缀&#10;&#10;- 修正开发者、小程序和企业相关API的基础URL,避免重复添加/api前缀&#10;- 统一调整developer、enterprise、invite等模块接口路径,保持与后端Controller一致&#10;- 新增权限申请及消息通知相关API,并完善相关函数实现&#10;- 规范请求参数传递,移除多余包装,改用直接传递对象方式&#10;- 新增API Key重置、版本发布/回滚等接口支持&#10;&#10;feat(invite): 重构邀请登录流程支持未注册快速加入&#10;&#10;- 完整重写invite页,分离已注册用户和未注册用户的按钮显示和交互&#10;- 实现未注册用户微信手机号授权后自动注册登录及自动加入应用&#10;- 已注册用户直接确认加入应用,无需手机号授权弹窗&#10;- 统一使用getPhoneNumber按钮处理两种状态并修复“授权码不能为空”报错&#10;- 前端配合后端改造,支持已登录用户可直接通过Authorization头加入应用&#10;&#10;fix(developer): 修复开发者中心应用加载问题&#10;&#10;- 修复只查询创建的应用导致邀请加入的应用不显示的问题&#10;- 同时请求创建应用和参与应用接口,合并并去重应用列表&#10;- 新增pageJoinedApp接口调用及合并逻辑,提高应用列表完整性和体验">
<option name="closed" value="true" />
<created>1782803958326</created>
<option name="number" value="00030" />
<option name="presentableId" value="LOCAL-00030" />
<option name="project" value="LOCAL" />
<updated>1782803958326</updated>
</task>
<task id="LOCAL-00031" summary="refactor(api): 规范前端API接口地址并统一路径前缀&#10;&#10;- 修正开发者、小程序和企业相关API的基础URL,避免重复添加/api前缀&#10;- 统一调整developer、enterprise、invite等模块接口路径,保持与后端Controller一致&#10;- 新增权限申请及消息通知相关API,并完善相关函数实现&#10;- 规范请求参数传递,移除多余包装,改用直接传递对象方式&#10;- 新增API Key重置、版本发布/回滚等接口支持&#10;&#10;feat(invite): 重构邀请登录流程支持未注册快速加入&#10;&#10;- 完整重写invite页,分离已注册用户和未注册用户的按钮显示和交互&#10;- 实现未注册用户微信手机号授权后自动注册登录及自动加入应用&#10;- 已注册用户直接确认加入应用,无需手机号授权弹窗&#10;- 统一使用getPhoneNumber按钮处理两种状态并修复“授权码不能为空”报错&#10;- 前端配合后端改造,支持已登录用户可直接通过Authorization头加入应用&#10;&#10;fix(developer): 修复开发者中心应用加载问题&#10;&#10;- 修复只查询创建的应用导致邀请加入的应用不显示的问题&#10;- 同时请求创建应用和参与应用接口,合并并去重应用列表&#10;- 新增pageJoinedApp接口调用及合并逻辑,提高应用列表完整性和体验">
<option name="closed" value="true" />
<created>1782803960387</created>
<option name="number" value="00031" />
<option name="presentableId" value="LOCAL-00031" />
<option name="project" value="LOCAL" />
<updated>1782803960387</updated>
</task>
<task id="LOCAL-00032" summary="refactor(api): 规范前端API接口地址并统一路径前缀&#10;&#10;- 修正开发者、小程序和企业相关API的基础URL,避免重复添加/api前缀&#10;- 统一调整developer、enterprise、invite等模块接口路径,保持与后端Controller一致&#10;- 新增权限申请及消息通知相关API,并完善相关函数实现&#10;- 规范请求参数传递,移除多余包装,改用直接传递对象方式&#10;- 新增API Key重置、版本发布/回滚等接口支持&#10;&#10;feat(invite): 重构邀请登录流程支持未注册快速加入&#10;&#10;- 完整重写invite页,分离已注册用户和未注册用户的按钮显示和交互&#10;- 实现未注册用户微信手机号授权后自动注册登录及自动加入应用&#10;- 已注册用户直接确认加入应用,无需手机号授权弹窗&#10;- 统一使用getPhoneNumber按钮处理两种状态并修复“授权码不能为空”报错&#10;- 前端配合后端改造,支持已登录用户可直接通过Authorization头加入应用&#10;&#10;fix(developer): 修复开发者中心应用加载问题&#10;&#10;- 修复只查询创建的应用导致邀请加入的应用不显示的问题&#10;- 同时请求创建应用和参与应用接口,合并并去重应用列表&#10;- 新增pageJoinedApp接口调用及合并逻辑,提高应用列表完整性和体验">
<option name="closed" value="true" />
<created>1782803963602</created>
<option name="number" value="00032" />
<option name="presentableId" value="LOCAL-00032" />
<option name="project" value="LOCAL" />
<updated>1782803963602</updated>
</task>
<task id="LOCAL-00033" summary="feat(app): 添加应用入口文件和配置文件&#10;&#10;- 新增 index.tsx 作为应用的主入口文件&#10;- 新增 index.config.ts 用于应用的配置管理">
<option name="closed" value="true" />
<created>1782804027400</created>
<option name="number" value="00033" />
<option name="presentableId" value="LOCAL-00033" />
<option name="project" value="LOCAL" />
<updated>1782804027400</updated>
</task>
<task id="LOCAL-00034" summary="feat(app): 添加应用入口文件和配置文件&#10;&#10;- 新增 index.tsx 作为应用的主入口文件&#10;- 新增 index.config.ts 用于应用的配置管理">
<option name="closed" value="true" />
<created>1782804461646</created>
<option name="number" value="00034" />
<option name="presentableId" value="LOCAL-00034" />
<option name="project" value="LOCAL" />
<updated>1782804461646</updated>
</task>
<task id="LOCAL-00035" summary="fix(tailwind): 更新配置以兼容微信小程序&#10;&#10;- 启用 autoprefixer 并新增 postcss-nested 插件支持嵌套语法&#10;- 扩展主题颜色,添加 primary 色系&#10;- 更新 content 路径包含 public/index.html&#10;- 禁用 preflight 以移除默认样式重置&#10;- 移除多数 corePlugins 禁用项,保留 preflight 禁用,简化兼容性配置">
<option name="closed" value="true" />
<created>1782805513768</created>
<option name="number" value="00035" />
<option name="presentableId" value="LOCAL-00035" />
<option name="project" value="LOCAL" />
<updated>1782805513768</updated>
</task>
<task id="LOCAL-00036" summary="feat(postcss): 添加插件移除微信小程序不兼容Tailwind规则&#10;&#10;- 实现 postcss-remove-wxss-incompatible 插件&#10;- 移除反斜杠转义的重要性修饰符类选择器(.\!xxx)&#10;- 删除依赖 :not 和兄弟选择器的 .space-[x|y]-* 规则&#10;- 删除依赖 :not 和兄弟选择器的 .divide-[x|y]-* 规则&#10;- 解决微信小程序 WXSS 不支持的 CSS 特性问题">
<option name="closed" value="true" />
<created>1782808735147</created>
<option name="number" value="00036" />
<option name="presentableId" value="LOCAL-00036" />
<option name="project" value="LOCAL" />
<updated>1782808735147</updated>
</task>
<task id="LOCAL-00037" summary="feat(postcss): 添加插件移除微信小程序不兼容Tailwind规则&#10;&#10;- 实现 postcss-remove-wxss-incompatible 插件&#10;- 移除反斜杠转义的重要性修饰符类选择器(.\!xxx)&#10;- 删除依赖 :not 和兄弟选择器的 .space-[x|y]-* 规则&#10;- 删除依赖 :not 和兄弟选择器的 .divide-[x|y]-* 规则&#10;- 解决微信小程序 WXSS 不支持的 CSS 特性问题">
<option name="closed" value="true" />
<created>1782808737652</created>
<option name="number" value="00037" />
<option name="presentableId" value="LOCAL-00037" />
<option name="project" value="LOCAL" />
<updated>1782808737652</updated>
</task>
<task id="LOCAL-00038" summary="feat(postcss): 添加插件移除微信小程序不兼容Tailwind规则&#10;&#10;- 实现 postcss-remove-wxss-incompatible 插件&#10;- 移除反斜杠转义的重要性修饰符类选择器(.\!xxx)&#10;- 删除依赖 :not 和兄弟选择器的 .space-[x|y]-* 规则&#10;- 删除依赖 :not 和兄弟选择器的 .divide-[x|y]-* 规则&#10;- 解决微信小程序 WXSS 不支持的 CSS 特性问题">
<option name="closed" value="true" />
<created>1782808741979</created>
<option name="number" value="00038" />
<option name="presentableId" value="LOCAL-00038" />
<option name="project" value="LOCAL" />
<updated>1782808741979</updated>
</task>
<task id="LOCAL-00039" summary="fix(pay): 优化订阅号获取逻辑并调整支付组件样式&#10;&#10;- 优先从小程序码 scene 参数读取 subscriptionNo,提升参数兼容性&#10;- 对 scene 参数进行解码,避免编码引起的问题&#10;- 移除支付组件价格显示处的固定颜色设置,增强样式灵活性">
<option name="closed" value="true" />
<created>1782810191833</created>
<option name="number" value="00039" />
<option name="presentableId" value="LOCAL-00039" />
<option name="project" value="LOCAL" />
<updated>1782810191833</updated>
</task>
<task id="LOCAL-00040" summary="fix(utils): 优化小程序端复制文本功能体验&#10;&#10;- 小程序端直接调用原生 wx.setClipboardData,避免 Taro Promise 封装问题&#10;- 避免微信小程序重复显示复制成功提示和误报复制失败&#10;- 失败时仅在小程序端输出错误日志,非小程序端显示提示&#10;- 修改首页外链跳转逻辑,支持相对路径跳转,小程序内外链复制提示提升用户体验&#10;- 更新项目内剪切板使用规范文档,说明复制行为调整及符合相应规范要求">
<option name="closed" value="true" />
<created>1782814230613</created>
<option name="number" value="00040" />
<option name="presentableId" value="LOCAL-00040" />
<option name="project" value="LOCAL" />
<updated>1782814230613</updated>
</task>
<task id="LOCAL-00041" summary="fix(utils): 优化小程序端复制文本功能体验&#10;&#10;- 小程序端直接调用原生 wx.setClipboardData,避免 Taro Promise 封装问题&#10;- 避免微信小程序重复显示复制成功提示和误报复制失败&#10;- 失败时仅在小程序端输出错误日志,非小程序端显示提示&#10;- 修改首页外链跳转逻辑,支持相对路径跳转,小程序内外链复制提示提升用户体验&#10;- 更新项目内剪切板使用规范文档,说明复制行为调整及符合相应规范要求">
<option name="closed" value="true" />
<created>1782814693982</created>
<option name="number" value="00041" />
<option name="presentableId" value="LOCAL-00041" />
<option name="project" value="LOCAL" />
<updated>1782814693982</updated>
</task>
<task id="LOCAL-00042" summary="fix(index): 优化外链提示及删除复制咨询模板功能&#10;&#10;- 替换小程序内外链打开方式提示为弹窗提醒用户手动复制&#10;- 删除复制咨询模板功能及相关按钮&#10;- 修改联系我们文案,突出直接电话沟通或需求反馈&#10;- 移除无用导入的复制文本工具函数copyText">
<option name="closed" value="true" />
<created>1782814719111</created>
<option name="number" value="00042" />
<option name="presentableId" value="LOCAL-00042" />
<option name="project" value="LOCAL" />
<updated>1782814719111</updated>
</task>
<task id="LOCAL-00043" summary="feat(utils): 优化请求函数URL拼接逻辑&#10;&#10;- 在request函数中将请求拦截改为先拼接完整URL&#10;- 支持相对路径自动补全baseUrl&#10;- 删除原有请求拦截中url未处理的问题">
<option name="closed" value="true" />
<created>1782879460607</created>
<option name="number" value="00043" />
<option name="presentableId" value="LOCAL-00043" />
<option name="project" value="LOCAL" />
<updated>1782879460607</updated>
</task>
<task id="LOCAL-00044" summary="feat(utils): 优化请求函数URL拼接逻辑&#10;&#10;- 在request函数中将请求拦截改为先拼接完整URL&#10;- 支持相对路径自动补全baseUrl&#10;- 删除原有请求拦截中url未处理的问题">
<option name="closed" value="true" />
<created>1782881600013</created>
<option name="number" value="00044" />
<option name="presentableId" value="LOCAL-00044" />
<option name="project" value="LOCAL" />
<updated>1782881600013</updated>
</task>
<task id="LOCAL-00045" summary="feat(utils): 优化请求函数URL拼接逻辑&#10;&#10;- 在request函数中将请求拦截改为先拼接完整URL&#10;- 支持相对路径自动补全baseUrl&#10;- 删除原有请求拦截中url未处理的问题">
<option name="closed" value="true" />
<created>1782881602732</created>
<option name="number" value="00045" />
<option name="presentableId" value="LOCAL-00045" />
<option name="project" value="LOCAL" />
<updated>1782881602732</updated>
</task>
<task id="LOCAL-00046" summary="feat(utils): 优化请求函数URL拼接逻辑&#10;&#10;- 在request函数中将请求拦截改为先拼接完整URL&#10;- 支持相对路径自动补全baseUrl&#10;- 删除原有请求拦截中url未处理的问题">
<option name="closed" value="true" />
<created>1782881620199</created>
<option name="number" value="00046" />
<option name="presentableId" value="LOCAL-00046" />
<option name="project" value="LOCAL" />
<updated>1782881620199</updated>
</task>
<task id="LOCAL-00047" summary="feat(utils): 优化请求函数URL拼接逻辑&#10;&#10;- 在request函数中将请求拦截改为先拼接完整URL&#10;- 支持相对路径自动补全baseUrl&#10;- 删除原有请求拦截中url未处理的问题">
<option name="closed" value="true" />
<created>1782881623335</created>
<option name="number" value="00047" />
<option name="presentableId" value="LOCAL-00047" />
<option name="project" value="LOCAL" />
<updated>1782881623335</updated>
</task>
<task id="LOCAL-00048" summary="feat(utils): 优化请求函数URL拼接逻辑&#10;&#10;- 在request函数中将请求拦截改为先拼接完整URL&#10;- 支持相对路径自动补全baseUrl&#10;- 删除原有请求拦截中url未处理的问题">
<option name="closed" value="true" />
<created>1782881646712</created>
<option name="number" value="00048" />
<option name="presentableId" value="LOCAL-00048" />
<option name="project" value="LOCAL" />
<updated>1782881646712</updated>
</task>
<task id="LOCAL-00049" summary="docs(schedule): 添加2026年7月1日日程文件&#10;&#10;- 创建新的日程文档文件 2026-07-01.md&#10;- 文件内容待后续补充和更新">
<option name="closed" value="true" />
<created>1782899810526</created>
<option name="number" value="00049" />
<option name="presentableId" value="LOCAL-00049" />
<option name="project" value="LOCAL" />
<updated>1782899810526</updated>
</task>
<task id="LOCAL-00050" summary="docs(schedule): 添加2026年7月1日日程文件&#10;&#10;- 创建新的日程文档文件 2026-07-01.md&#10;- 文件内容待后续补充和更新">
<option name="closed" value="true" />
<created>1782899812498</created>
<option name="number" value="00050" />
<option name="presentableId" value="LOCAL-00050" />
<option name="project" value="LOCAL" />
<updated>1782899812498</updated>
</task>
<task id="LOCAL-00051" summary="feat(pay): 优化小程序支付流程并增强日志调试&#10;&#10;- 更新 wx-login 接口以返回完整 LoginResult,包括 access_token 和 user 对象&#10;- 支付页面新增大量日志,详尽打印参数、请求和响应信息&#10;- 优化获取订阅详情流程,支持 returnRaw 返回完整响应用于调试&#10;- 支付流程新增步骤,先通过 code 获取 openid 并缓存,再尝试用 openid 自动登录&#10;- 增加自动登录失败交互,提示未注册用户先登录&#10;- 支付相关请求均使用 returnRaw 方便错误排查&#10;- request 请求模块添加统一请求和响应日志,增强调试能力&#10;- request 认证错误和业务错误时加入详细日志输出">
<option name="closed" value="true" />
<created>1782979300991</created>
<option name="number" value="00051" />
<option name="presentableId" value="LOCAL-00051" />
<option name="project" value="LOCAL" />
<updated>1782979300991</updated>
</task>
<task id="LOCAL-00052" summary="feat(pay): 支付成功后自动跳转到已购产品页面&#10;&#10;- 支付成功后显示成功提示&#10;- 延迟1.5秒自动跳转到已购产品页面&#10;- 处理跳转失败时回退到用户首页&#10;- 优化用户支付完成后的使用体验">
<option name="closed" value="true" />
<created>1783003209100</created>
<option name="number" value="00052" />
<option name="presentableId" value="LOCAL-00052" />
<option name="project" value="LOCAL" />
<updated>1783003209100</updated>
</task>
<task id="LOCAL-00053" summary="feat(pay-bak): 新增小程序支付页面及相关配置&#10;&#10;- 新增确认支付页面配置,设置导航栏标题和样式&#10;- 实现小程序支付页完整组件逻辑,包括加载订单、发起JSAPI支付等&#10;- 支持通过 scene 或 subscriptionNo 获取订单参数&#10;- 完善支付流程接口请求及错误处理,支持自动登录和支付确认&#10;- 增加支付成功、支付中、支付失败、取消等状态页面展示&#10;- 支持支付成功后跳转已购产品页或用户页&#10;- 提供取消支付返回逻辑和界面展示&#10;- 细化支付金额显示和提示用户支付后可使用应用功能">
<option name="closed" value="true" />
<created>1784131564427</created>
<option name="number" value="00053" />
<option name="presentableId" value="LOCAL-00053" />
<option name="project" value="LOCAL" />
<updated>1784131564427</updated>
</task>
<task id="LOCAL-00054" summary="feat(address): 新增收货地址与售后申请功能&#10;&#10;- 新增地址编辑页面,支持地址智能识别、地图选点、地区选择和默认地址设置&#10;- 实现地址列表页面,支持地址查看、删除、设为默认及选择返回结算页&#10;- 新增售后申请页面,支持退款类型选择、商品选择、原因填写、图片上传和提交审核&#10;- 修复 passport 分包配置,移除不存在分包并补充缺失声明,避免 Taro 编译报错&#10;- 新增地址类型定义,增强前端地址数据结构类型安全&#10;- 优化页面交互体验,完善表单校验及错误提示逻辑&#10;- 统一代码格式与命名规范,保持代码风格一致性">
<option name="closed" value="true" />
<created>1784135922873</created>
<option name="number" value="00054" />
<option name="presentableId" value="LOCAL-00054" />
<option name="project" value="LOCAL" />
<updated>1784135922873</updated>
</task>
<option name="localTasksCounter" value="55" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="Vcs.Log.Tabs.Properties">
<option name="TAB_STATES">
<map>
<entry key="MAIN">
<value>
<State />
</value>
</entry>
</map>
</option>
</component>
<component name="VcsManagerConfiguration">
<option name="CHECK_CODE_SMELLS_BEFORE_PROJECT_COMMIT" value="false" />
<option name="CHECK_NEW_TODO" value="false" />
<MESSAGE value="fix(invite): 修正邀请接口路径以支持小程序专用前缀&#10;&#10;- 将请求邀请信息接口路径改为使用 /api/_app 前缀,确保小程序免登录访问&#10;- 修改接受邀请接口路径,统一使用小程序专用 API 域名和前缀&#10;- 更新邀请相关接口注释,明确小程序专用接口前缀含义&#10;- 在专家历史数据中添加高级开发工程师 Will 的条目并更新时间戳" />
<MESSAGE value="fix(passport): 添加邀请接受接口调试日志&#10;&#10;- 在接受邀请请求前打印token和请求URL、参数&#10;- 在请求响应后输出接口响应及返回数据&#10;- 在调用失败时打印错误信息及异常日志&#10;- 保持请求流程和状态管理不变,增强调试能力" />
<MESSAGE value="fix(passport): 优化邀请码接受失败的用户提示和处理&#10;&#10;- 针对“用户不存在”、“用户创建失败”及“请先登录”提示,改为弹窗提示需登录&#10;- 在弹窗中增加“去登录”按钮,点击后跳转到登录页面,并携带重定向token参数&#10;- 其他错误仍使用原Toast提示方式展示错误信息&#10;- 保留错误日志打印,便于排查异常情况" />
<MESSAGE value="feat(passport-invite): 优化新用户扫码邀请加入流程&#10;&#10;- 扫码进入邀请页面时保存邀请 token 到本地存储&#10;- 新增登录状态检测,未登录用户点击快速加入时保存邀请信息并跳转登录页&#10;- 登录成功后返回邀请页自动执行加入应用操作,提升用户体验&#10;- 支持已登录用户直接加入应用,简化流程&#10;- 增加清理 pending 邀请数据的方法,防止数据残留&#10;- 监听页面显示事件,处理登录返回后自动操作&#10;- 调整接口请求逻辑,确保携带最新的邀请和授权数据&#10;- 更新项目配置,注册邀请页面路径信息" />
<MESSAGE value="feat(passport): 重写登录与邀请加入流程,支持微信手机号一键登录&#10;&#10;- 完全重写 passport/login.tsx,实现微信手机号一键登录功能&#10;- 支持登录页面携带 redirect 参数,登录后自动跳转回原页面&#10;- 登录成功后自动处理邀请加入应用,支持邀请关系绑定&#10;- 重写 passport/invite/index.tsx,实现扫码邀请加入应用完整流程&#10;- 支持调用 wx.login 获取 code,使用 loginByOpenId 判断注册状态&#10;- 未注册用户自动跳转登录页完成微信手机号登录再加入应用&#10;- 已登录用户直接执行加入应用操作,加入接口支持 Authorization 头&#10;-" />
<MESSAGE value="feat(invite): 重构邀请加入流程,实现已登录用户免授权直接加入&#10;&#10;- 完全重写邀请页逻辑,区分已登录与未注册用户按钮显示&#10;- 已登录用户显示“确认加入”按钮,点击用 wx.login 获取 code 加入&#10;- 未注册用户显示“微信手机号快速加入”按钮,授权手机号后完成注册登录再加入&#10;- 移除跳转登录页面逻辑,所有流程在邀请页内完成&#10;- 加入接口统一使用 Authorization 头携带 token,确保身份双重验证&#10;- 优化背景、粒子和扫描线效果,提升界面视觉体验&#10;- 增加错误处理提示,加入失败时显示详细原因并可返回首页&#10;- 协议勾选仅在未注册用户显示,确保合规授权流程" />
<MESSAGE value="feat(passport): 优化已登录用户加入时协议勾选逻辑&#10;&#10;- 已登录用户点击「确认加入」时不再强制勾选协议&#10;- 移除 handleConfirmJoin 中对协议勾选的检查&#10;- 未注册用户仍需勾选协议后才能授权手机号&#10;- 确保登录状态异常时有提示信息显示" />
<MESSAGE value="feat(invite): 支持已登录用户直接加入应用&#10;&#10;- 后端accept接口code参数改为可选,支持通过Authorization头识别登录用户&#10;- 已登录用户加入时不再弹手机号授权,前端调用新增的doJoinAppForLoggedInUser方法&#10;- 未注册用户仍通过手机号授权码code加入,调用doJoinAppForNewUser方法&#10;- 拆分前端加入逻辑,新增handleConfirmJoin统一处理已登录加入流程&#10;- 优化按钮交互,已登录状态下按钮变为普通点击,移除协议强制勾选&#10;- 错误处理和状态提示完善,加入成功后自动跳转首页" />
<MESSAGE value="feat(developer): 完成小程序开发者中心和企业控制台改造&#10;&#10;- 设计并实现了开发者中心与企业控制台两大模块&#10;- 按用户角色区分开发者和企业客户,支持多项目类型及成员管理&#10;- 新增项目管理、应用管理、API Key管理及成员邀请等多功能页面&#10;- 实现应用版本发布、消息通知中心、权限审批与开发者申请流程&#10;- 完成CI/CD流水线、运营监控、发票管理、SSO单点登录功能&#10;- 搭建SDK下载中心、工单系统、FAQ系统、数据导入导出等模块&#10;- 优化后端API,支持已登录和未注册用户不同加入应用流程&#10;- 前端按钮统一采用微信手机号授权,完善用户授权体验&#10;- 修复多个页面的JSX语法错误及依赖导入问题,替换部分组件库&#10;- 增加详细的类型定义文件,提升项目类型安全&#10;- 新增超过55个页面及60个API接口,扩展应用功能和服务体系&#10;- 完成全面的样式设计,实现一致的视觉风格和交互体验" />
<MESSAGE value="feat(api-keys): 优化隐私协议同意逻辑及样式&#10;&#10;- 修正api-keys样式导入路径,改为developer路径&#10;- 在开发者申请页面增加服务协议和隐私政策勾选框&#10;- 提交时校验协议是否已勾选,未勾选时提示用户&#10;- 优化申请页面协议区域样式及交互&#10;- 在用户验证页面添加隐私政策同意勾选框&#10;- 提交用户验证表单时检查协议同意状态&#10;- 增加打开服务协议和隐私政策的链接跳转&#10;- 更新项目配置,增加passport/webview路由配置&#10;- 修复createTicket函数中日期格式正则表达式问题" />
<MESSAGE value="refactor(config): 移除开发者中心和企业控制台的自定义导航样式&#10;&#10;- 删除了开发者中心页面配置中的 navigationStyle: 'custom'&#10;- 删除了企业控制台页面配置中的 navigationStyle: 'custom'&#10;- 简化了页面配置,保持默认导航样式&#10;- 有助于提升页面兼容性和维护性" />
<MESSAGE value="feat(userVerify): 添加阿里云身份证二要素核验功能&#10;&#10;- 在接口层新增 verifyIdCard 接口,实现姓名与身份证号核验&#10;- userVerify 页面提交时增加身份核验逻辑,核验失败阻止提交&#10;- 调用核验接口时显示加载状态,并根据结果提示用户&#10;- 后端依赖 aliyun cloudauth,提供实人认证相关服务接口&#10;- 完成基本的错误捕获与用户提示,提升实名认证流程安全性" />
<MESSAGE value="refactor(api): 规范前端API接口地址并统一路径前缀&#10;&#10;- 修正开发者、小程序和企业相关API的基础URL,避免重复添加/api前缀&#10;- 统一调整developer、enterprise、invite等模块接口路径,保持与后端Controller一致&#10;- 新增权限申请及消息通知相关API,并完善相关函数实现&#10;- 规范请求参数传递,移除多余包装,改用直接传递对象方式&#10;- 新增API Key重置、版本发布/回滚等接口支持&#10;&#10;feat(invite): 重构邀请登录流程支持未注册快速加入&#10;&#10;- 完整重写invite页,分离已注册用户和未注册用户的按钮显示和交互&#10;- 实现未注册用户微信手机号授权后自动注册登录及自动加入应用&#10;- 已注册用户直接确认加入应用,无需手机号授权弹窗&#10;- 统一使用getPhoneNumber按钮处理两种状态并修复“授权码不能为空”报错&#10;- 前端配合后端改造,支持已登录用户可直接通过Authorization头加入应用&#10;&#10;fix(developer): 修复开发者中心应用加载问题&#10;&#10;- 修复只查询创建的应用导致邀请加入的应用不显示的问题&#10;- 同时请求创建应用和参与应用接口,合并并去重应用列表&#10;- 新增pageJoinedApp接口调用及合并逻辑,提高应用列表完整性和体验" />
<MESSAGE value="feat(app): 添加应用入口文件和配置文件&#10;&#10;- 新增 index.tsx 作为应用的主入口文件&#10;- 新增 index.config.ts 用于应用的配置管理" />
<MESSAGE value="fix(tailwind): 更新配置以兼容微信小程序&#10;&#10;- 启用 autoprefixer 并新增 postcss-nested 插件支持嵌套语法&#10;- 扩展主题颜色,添加 primary 色系&#10;- 更新 content 路径包含 public/index.html&#10;- 禁用 preflight 以移除默认样式重置&#10;- 移除多数 corePlugins 禁用项,保留 preflight 禁用,简化兼容性配置" />
<MESSAGE value="feat(postcss): 添加插件移除微信小程序不兼容Tailwind规则&#10;&#10;- 实现 postcss-remove-wxss-incompatible 插件&#10;- 移除反斜杠转义的重要性修饰符类选择器(.\!xxx)&#10;- 删除依赖 :not 和兄弟选择器的 .space-[x|y]-* 规则&#10;- 删除依赖 :not 和兄弟选择器的 .divide-[x|y]-* 规则&#10;- 解决微信小程序 WXSS 不支持的 CSS 特性问题" />
<MESSAGE value="fix(pay): 优化订阅号获取逻辑并调整支付组件样式&#10;&#10;- 优先从小程序码 scene 参数读取 subscriptionNo,提升参数兼容性&#10;- 对 scene 参数进行解码,避免编码引起的问题&#10;- 移除支付组件价格显示处的固定颜色设置,增强样式灵活性" />
<MESSAGE value="fix(utils): 优化小程序端复制文本功能体验&#10;&#10;- 小程序端直接调用原生 wx.setClipboardData,避免 Taro Promise 封装问题&#10;- 避免微信小程序重复显示复制成功提示和误报复制失败&#10;- 失败时仅在小程序端输出错误日志,非小程序端显示提示&#10;- 修改首页外链跳转逻辑,支持相对路径跳转,小程序内外链复制提示提升用户体验&#10;- 更新项目内剪切板使用规范文档,说明复制行为调整及符合相应规范要求" />
<MESSAGE value="fix(index): 优化外链提示及删除复制咨询模板功能&#10;&#10;- 替换小程序内外链打开方式提示为弹窗提醒用户手动复制&#10;- 删除复制咨询模板功能及相关按钮&#10;- 修改联系我们文案,突出直接电话沟通或需求反馈&#10;- 移除无用导入的复制文本工具函数copyText" />
<MESSAGE value="feat(utils): 优化请求函数URL拼接逻辑&#10;&#10;- 在request函数中将请求拦截改为先拼接完整URL&#10;- 支持相对路径自动补全baseUrl&#10;- 删除原有请求拦截中url未处理的问题" />
<MESSAGE value="docs(schedule): 添加2026年7月1日日程文件&#10;&#10;- 创建新的日程文档文件 2026-07-01.md&#10;- 文件内容待后续补充和更新" />
<MESSAGE value="feat(pay): 优化小程序支付流程并增强日志调试&#10;&#10;- 更新 wx-login 接口以返回完整 LoginResult,包括 access_token 和 user 对象&#10;- 支付页面新增大量日志,详尽打印参数、请求和响应信息&#10;- 优化获取订阅详情流程,支持 returnRaw 返回完整响应用于调试&#10;- 支付流程新增步骤,先通过 code 获取 openid 并缓存,再尝试用 openid 自动登录&#10;- 增加自动登录失败交互,提示未注册用户先登录&#10;- 支付相关请求均使用 returnRaw 方便错误排查&#10;- request 请求模块添加统一请求和响应日志,增强调试能力&#10;- request 认证错误和业务错误时加入详细日志输出" />
<MESSAGE value="feat(pay): 支付成功后自动跳转到已购产品页面&#10;&#10;- 支付成功后显示成功提示&#10;- 延迟1.5秒自动跳转到已购产品页面&#10;- 处理跳转失败时回退到用户首页&#10;- 优化用户支付完成后的使用体验" />
<MESSAGE value="feat(pay-bak): 新增小程序支付页面及相关配置&#10;&#10;- 新增确认支付页面配置,设置导航栏标题和样式&#10;- 实现小程序支付页完整组件逻辑,包括加载订单、发起JSAPI支付等&#10;- 支持通过 scene 或 subscriptionNo 获取订单参数&#10;- 完善支付流程接口请求及错误处理,支持自动登录和支付确认&#10;- 增加支付成功、支付中、支付失败、取消等状态页面展示&#10;- 支持支付成功后跳转已购产品页或用户页&#10;- 提供取消支付返回逻辑和界面展示&#10;- 细化支付金额显示和提示用户支付后可使用应用功能" />
<MESSAGE value="feat(address): 新增收货地址与售后申请功能&#10;&#10;- 新增地址编辑页面,支持地址智能识别、地图选点、地区选择和默认地址设置&#10;- 实现地址列表页面,支持地址查看、删除、设为默认及选择返回结算页&#10;- 新增售后申请页面,支持退款类型选择、商品选择、原因填写、图片上传和提交审核&#10;- 修复 passport 分包配置,移除不存在分包并补充缺失声明,避免 Taro 编译报错&#10;- 新增地址类型定义,增强前端地址数据结构类型安全&#10;- 优化页面交互体验,完善表单校验及错误提示逻辑&#10;- 统一代码格式与命名规范,保持代码风格一致性" />
<option name="LAST_COMMIT_MESSAGE" value="feat(address): 新增收货地址与售后申请功能&#10;&#10;- 新增地址编辑页面,支持地址智能识别、地图选点、地区选择和默认地址设置&#10;- 实现地址列表页面,支持地址查看、删除、设为默认及选择返回结算页&#10;- 新增售后申请页面,支持退款类型选择、商品选择、原因填写、图片上传和提交审核&#10;- 修复 passport 分包配置,移除不存在分包并补充缺失声明,避免 Taro 编译报错&#10;- 新增地址类型定义,增强前端地址数据结构类型安全&#10;- 优化页面交互体验,完善表单校验及错误提示逻辑&#10;- 统一代码格式与命名规范,保持代码风格一致性" />
<option name="NON_MODAL_COMMIT_POSTPONE_SLOW_CHECKS" value="false" />
</component>
</project>
-61
View File
@@ -1,61 +0,0 @@
{
"version": 2,
"sessions": {
"22fe5580026e4f249cf9bb33390baa6a": [
{
"expertId": "SeniorDeveloper",
"name": "Will",
"profession": "高级开发工程师",
"avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/SeniorDeveloper/SeniorDeveloper.png",
"promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/SeniorDeveloper/SeniorDeveloper_zh.md",
"usedAt": 1775908159660,
"industryId": "all"
}
],
"751f0e28e31849adb28b750fe4cdcde7": [
{
"expertId": "SeniorDeveloper",
"name": "Will",
"profession": "高级开发工程师",
"avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/SeniorDeveloper/SeniorDeveloper.png",
"promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/SeniorDeveloper/SeniorDeveloper_zh.md",
"usedAt": 1775908159660,
"industryId": "all"
}
],
"f2494c1730eb411aac709ec2751d60fc": [
{
"expertId": "SeniorDeveloper",
"name": "Will",
"profession": "高级开发工程师",
"avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/SeniorDeveloper/SeniorDeveloper.png",
"promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/SeniorDeveloper/SeniorDeveloper_zh.md",
"usedAt": 1775921148885,
"industryId": "all"
}
],
"784e1c35ace143c4bd1aad6e187435b1": [
{
"expertId": "SeniorDeveloper",
"name": "Will",
"profession": "高级开发工程师",
"avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/SeniorDeveloper/SeniorDeveloper.png",
"promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/SeniorDeveloper/SeniorDeveloper_zh.md",
"usedAt": 1775972794982,
"industryId": "all"
}
],
"e1bb762122264b5d9d218f575fecf04b": [
{
"expertId": "SeniorDeveloper",
"name": "Will",
"profession": "高级开发工程师",
"avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/SeniorDeveloper/SeniorDeveloper.png",
"promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/SeniorDeveloper/SeniorDeveloper_zh.md",
"usedAt": 1776019266960,
"industryId": "all"
}
]
},
"lastUpdated": 1776023218479
}
-253
View File
@@ -1,253 +0,0 @@
# 2026-04-11 工作记录
## 任务:修复邀请加入应用流程
### 问题描述
`passport/invite/index` 页面在新用户扫码加入应用时存在问题:
- 新用户点击"微信手机号快速加入"时,如果未注册,后端返回"用户不存在"
- 页面引导用户去登录,但登录完成后**没有自动执行加入应用的操作**
### 解决方案
参考 `UserCard.tsx` 和 `phone-auth/index.tsx` 的实现模式,修改了 `src/passport/invite/index.tsx`:
#### 1. 新增功能
- 页面加载时保存邀请 token 到本地存储
- 检测用户登录状态
- 未登录用户点击加入时,保存邀请信息并引导到 `phone-auth` 登录页
- 登录成功后返回邀请页面,自动执行加入操作
- 新增 `clearPendingInviteData` 方法清理临时数据
#### 2. 关键修改点
- 使用 `saveInviteParams` 保存邀请信息
- 使用 `pending_invite_*` 系列 storage key 保存待处理的邀请数据
- 页面显示时通过 `AppShow` 事件监听登录返回
- 登录成功后自动调用 `handleJoinApp` 完成加入
#### 3. 流程优化
```
新用户流程:
1. 扫码进入邀请页面 → 保存 invite token
2. 点击"微信手机号快速加入"
3. 检测到未登录 → 保存 pending 数据 → 跳转到 phone-auth
4. 完成登录/注册 → 返回邀请页面
5. 自动检测 pending 数据 → 自动执行加入
6. 加入成功 → 跳转到首页
已登录用户流程:
1. 扫码进入邀请页面
2. 点击"微信手机号快速加入"
3. 直接执行加入操作
4. 加入成功 → 跳转到首页
```
### 文件修改
- `src/passport/invite/index.tsx` - 完整重构邀请流程
---
## 任务:重写 passport/login 页面
### 问题描述
`passport/login.tsx` 页面只有 UI 框架,没有实现登录逻辑:
- 输入框没有绑定状态
- 登录按钮没有点击事件
- 不支持微信手机号登录
- 无法处理邀请流程的重定向
### 解决方案
基于 `phone-auth/index.tsx` 的实现,重写 `login.tsx`:
#### 1. 功能实现
- 微信手机号一键登录(使用 `open-type="getPhoneNumber"`)
- 支持 `redirect` 参数,登录后返回原页面
- 未注册用户自动注册
- 支持邀请关系绑定
- 服务协议和隐私政策弹窗
#### 2. 跳转逻辑
```
有 redirect 参数:
- TabBar 页面 → switchTab
- 普通页面 → navigateBack 或 redirectTo
无 redirect 参数:
- 跳转到首页 /pages/index/index
```
#### 3. 统一登录入口
- 将 `invite/index.tsx` 中的登录跳转统一改为 `/passport/login`
- 保持与现有代码引用兼容(5处引用无需修改)
### 文件修改
- `src/passport/login.tsx` - 完全重写为微信手机号登录页面
- `src/passport/invite/index.tsx` - 更新登录跳转链接
---
## 任务:修复登录后加入应用流程
### 问题描述
用户从邀请页面跳转到登录页面完成注册后,返回邀请页面提示"您尚未注册",无法成功加入应用。
### 问题分析
1. 用户在邀请页面点击"微信手机号快速加入"时保存了微信授权码
2. 跳转到登录页面后,用户**重新**进行了微信授权,获得了新的授权码
3. 登录成功后返回邀请页面,但邀请页面使用的是旧的已失效的授权码
4. 导致加入应用失败
### 解决方案
在登录页面登录成功后,直接使用**当前获取的微信授权码**完成加入应用操作:
#### 1. 登录页面修改 (`login.tsx`)
- 添加 `handleJoinAppAfterLogin` 方法
- 登录成功后检测 `pending_invite_token`
- 如果存在,使用当前授权码调用 `/api/_app/developer/invite/accept` 接口
- 加入成功后清理 pending 数据并跳转到首页
- 加入失败则继续正常登录流程
#### 2. 邀请页面修改 (`invite/index.tsx`)
- 优化从登录页返回的处理逻辑
- 检测 `pending_invite_phone_code` 是否存在
- 延迟检查登录页面是否已处理加入操作
- 如果登录页面已处理(token 被清除),则显示成功提示并跳转
- 如果登录页面未处理,则自动执行加入操作
### 新的完整流程
```
新用户扫码加入流程:
1. 扫码进入邀请页面
2. 点击"微信手机号快速加入"
3. 检测到未登录,保存 pending 数据,跳转到登录页面
4. 在登录页面勾选协议,点击"微信手机号一键登录"
5. 获取新的微信授权码,完成登录/注册
6. 登录成功后检测到 pending_invite_token,自动执行加入应用
7. 加入成功,清理数据,跳转到首页
已登录用户流程:
1. 扫码进入邀请页面
2. 点击"微信手机号快速加入"
3. 直接执行加入操作
4. 加入成功,跳转到首页
```
### 文件修改
- `src/passport/login.tsx` - 添加登录后自动加入应用逻辑
- `src/passport/invite/index.tsx` - 优化从登录页返回的处理逻辑
---
## 任务:修复"用户创建失败"问题
### 问题描述
后端返回 `"用户创建失败"`,原因是微信授权码失效。
### 问题分析
1. 用户在邀请页面获取了微信授权码
2. 跳转到登录页面后,用户**重新**点击授权按钮获取了新的授权码
3. 但代码逻辑可能混淆了新旧授权码
4. 或者授权码已过期(5分钟有效期)
### 解决方案
1. **明确使用当前获取的授权码**:在登录页面登录成功后,使用**当前**获取的微信授权码执行加入应用操作
2. **不使用旧的授权码**:从邀请页面带过来的授权码仅作为标记用途,实际使用登录时获取的新授权码
3. **添加错误处理**:加入失败时显示错误信息并延迟跳转
### 关键修改
- `login.tsx`:优化 `handleLogin` 中的邀请流程处理逻辑
- 明确使用当前 `phoneCode` 执行加入操作
- 添加错误处理和用户提示
- 加入失败后延迟跳转,让用户看到错误信息
---
## 任务:修复已登录用户加入应用失败问题
### 问题描述
已登录用户点击"微信手机号快速加入"时,后端返回 `"用户创建失败"`。
### 问题分析
1. 用户已经是登录状态(有 `access_token`)
2. 但后端接口 `/api/_app/developer/invite/accept` 仍然尝试"创建用户"
3. 原因是后端无法识别当前已登录的用户身份
### 解决方案
为已登录用户在请求头中添加 `Authorization: Bearer {access_token}`,让后端能正确识别用户身份:
#### 1. 邀请页面修改 (`invite/index.tsx`)
- 在 `handleJoinApp` 方法中获取 `access_token`
- 构建请求头时,如果用户已登录,添加 `Authorization` 头
- 添加日志记录是否使用了认证头
#### 2. 登录页面修改 (`login.tsx`)
- 在 `handleJoinAppAfterLogin` 方法中同样添加 `Authorization` 头
- 确保登录成功后调用加入接口时携带认证信息
### 文件修改
- `src/passport/invite/index.tsx` - 添加 Authorization 请求头支持
- `src/passport/login.tsx` - 添加 Authorization 请求头支持
---
## 任务:修复 401 认证失败问题
### 问题描述
后端返回 `401` 和 `"请退出重新登录"`,`error: "Username not found"`。
### 问题分析
1. 请求带了 `Authorization: Bearer {token}` 头
2. 但后端验证 token 时发现用户不存在
3. 原因可能是:
- token 已过期
- token 对应的用户已被删除
- `/api/_app` 前缀的接口设计为"免登录",使用 `Authorization` 头反而导致认证失败
### 解决方案
`/api/_app` 前缀的接口是小程序专用免登录接口,应该优先使用微信授权码方式:
#### 策略调整
- **有微信授权码**:使用授权码方式,不传 `Authorization` 头
- **无授权码但已登录**:使用 `Authorization` 头
#### 修改内容
- `invite/index.tsx`:调整 `handleJoinApp` 方法中的请求头逻辑
- 优先使用微信授权码,避免使用可能过期的 token
### 文件修改
- `src/passport/invite/index.tsx` - 调整认证策略,优先使用微信授权码
---
## 任务:重写邀请加入应用流程
### 新流程设计
1. 扫码进入邀请页面
2. 调用 `wx.login()` 获取 code
3. 调用 `loginByOpenId` 判断用户是否已注册
4. 已注册:显示邀请页面,用户点击加入
5. 未注册:跳转到 `passport/login` 页面完成登录/注册
6. 登录成功后自动执行加入应用操作
### 实现细节
#### 1. 邀请页面 (`invite/index.tsx`)
- 页面状态管理:`loading` → `checking` → `invite`/`login`/`error`
- `initPage()`:解析 token,保存到 storage
- `checkLoginStatus()`:调用 `loginByOpenId` 检查登录状态
- `navigateToLogin()`:未登录用户跳转到登录页
- `fetchInviteInfo()`:获取邀请信息
- `handleGetPhoneNumber()`:处理微信授权
- `handleJoinApp()`:执行加入应用操作
#### 2. 登录页面 (`login.tsx`)
- 登录成功后检查 `invite_token`
- 如果存在,使用当前授权码自动执行加入应用
- 加入成功后跳转到首页
### 简化点
- 不再使用复杂的 `pending_invite_*` 系列 storage key
- 只使用 `invite_token` 保存邀请标识
- 登录页面直接使用当前获取的微信授权码
### 文件修改
- `src/passport/invite/index.tsx` - 完全重写
- `src/passport/login.tsx` - 更新为使用 `invite_token`
-615
View File
@@ -1,615 +0,0 @@
# 2026-04-12 工作日志
## 项目:小程序改造规划
### 背景
用户需要为 websopy-mp 小程序增加:
- 🛠️ **开发者中心** - 面向开发者,围绕项目展开
- 🏢 **企业控制台** - 面向企业客户
### 关键决策
1. **用户角色分两类**:
- 开发者 (Developer):独立于企业存在,申请审核制
- 企业客户 (Enterprise):属于企业组织,购买产品开通
2. **开发模式**:独立开发,不复用现有模块(商城/分销/用户订单)
3. **核心围绕"项目"展开**:
- 项目类型:基础/专业/企业
- 项目成员、API Key、应用关联
### Phase 1 开发完成 ✅
已完成以下功能开发:
#### 新增目录结构
```
src/
├── developer/ # 🛠️ 开发者模块
│ ├── index.tsx # 开发者工作台
│ ├── project/ # 项目管理
│ │ ├── index.tsx # 项目列表
│ │ ├── create.tsx # 创建项目
│ │ └── [id]/ # 项目详情
│ ├── app/ # 应用管理
│ │ ├── index.tsx # 应用列表
│ │ ├── create.tsx # 创建应用
│ │ ├── [id]/ # 应用详情
│ │ └── api-keys/ # API Key 管理
│ └── docs/ # 开发者文档
│
├── enterprise/ # 🏢 企业模块
│ ├── index.tsx # 企业工作台
│ ├── apps/ # 企业应用
│ │ ├── index.tsx # 应用列表
│ │ ├── [id]/ # 应用详情
│ │ └── purchase/ # 购买应用
│ ├── members/ # 成员管理
│ │ ├── index.tsx # 成员列表
│ │ └── invite/ # 邀请成员
│ ├── orders/ # 订单账单
│ │ ├── index.tsx # 订单列表
│ │ └── detail/ # 订单详情
│ ├── billing/ # 费用中心
│ └── settings/ # 企业设置
│
├── api/developer/ # API 模块
│ ├── index.ts
│ ├── developer.ts # 开发者 API
│ └── enterprise.ts # 企业 API
│
└── types/developer.ts # 类型定义
```
#### 新增页面
- 首页角色入口卡片(开发者中心/企业控制台)
- 开发者工作台 + 统计卡片 + 快捷操作
- 项目列表/创建/详情
- 应用列表/创建/详情
- 企业工作台 + 统计卡片 + 快捷操作
- 企业应用列表/购买/详情
- 成员列表/邀请
- 订单列表/详情(占位)
- 费用中心/企业设置(占位)
#### 新增 API
- 项目管理 API
- 应用管理 API
- API Key 管理 API
- 企业成员 API
- 订单账单 API
### 待确认
- 后端 API 接口地址是否正确
- 是否需要权限验证
- 微信小程序码支持
---
## Phase 2 开发完成 ✅ (2026-04-12 19:49)
### 新增功能模块
#### 1. API Key 完整管理
- 列表展示(名称/类型/状态/创建时间)
- 创建 Key(名称/类型/备注)
- 复制 AppId/AppSecret
- 删除 Key
- 状态筛选
#### 2. 项目成员管理
- 成员列表展示(头像/名称/角色)
- 角色统计(所有者/管理员/开发者/查看者)
- 邀请成员(用户名/角色选择)
- 修改成员角色
- 移除成员
#### 3. 应用版本发布
- 版本列表(版本号/状态/环境)
- 环境 Tab 筛选(全部/开发/预发布/生产)
- 发布新版本(名称/版本号/环境/更新日志)
- 状态标签(构建中/已发布/已回滚/构建失败)
#### 4. 消息通知中心
- 通知列表(类型/标题/内容/时间)
- 类型筛选(全部/系统/应用/成员/订单)
- 未读数量徽标
- 标记已读/全部已读
- 删除通知
- 点击跳转详情
#### 5. 开发者申请
- 申请表单(姓名/手机/邮箱/公司/职位/经验)
- 申请记录列表
- 状态筛选(待审核/已通过/已拒绝)
- 审核备注展示
#### 6. 权限审批
- 申请列表(申请人/类型/目标/状态)
- Tab 筛选(待审核/已通过/已拒绝/全部)
- 待审核数量徽标
- 通过/拒绝操作
- 审核备注展示
### 新增/扩展的页面
```
src/developer/
├── project/[id]/
│ ├── members.tsx # 项目成员管理 ✅
│ ├── members.scss
│ ├── api-keys.tsx # 项目级 API Key
│ ├── api-keys.scss
│ ├── api-keys.config.ts
│ ├── settings.tsx # 项目设置
│ └── settings.scss
├── app/[id]/
│ ├── version.tsx # 版本管理 ✅
│ ├── version.scss
│ ├── config.tsx # 应用配置
│ ├── publish.tsx # 发布管理
│ └── *.config.ts
├── notification/ # 消息通知 ✅
│ ├── index.tsx
│ └── index.scss
├── developer/
│ ├── apply.tsx # 开发者申请 ✅
│ ├── apply.scss
│ └── profile.tsx # 开发者资料
├── docs/
│ ├── quickstart.tsx # 快速开始
│ └── api-docs.tsx # API 文档
└── market/ # 开发者市场
└── index.tsx
src/enterprise/
├── apps/[id]/
│ ├── monitor.tsx # 运营监控
│ ├── analytics.tsx # 数据分析
│ └── settings.tsx # 应用设置
├── members/
│ ├── roles.tsx # 角色权限
│ └── audit.tsx # 权限审批 ✅
├── orders/
│ ├── invoice.tsx # 发票管理
│ └── bills.tsx # 账单明细
├── billing/
│ ├── consumption.tsx # 消费明细
│ ├── recharge.tsx # 充值中心
│ └── coupons.tsx # 优惠券
├── settings/
│ ├── info.tsx # 企业信息
│ ├── domain.tsx # 域名配置
│ └── security.tsx # 安全设置
└── developer/
├── index.tsx # 开发者入口
└── apply.tsx # 申请开发者
```
### 扩展的类型定义
```typescript
// 消息通知
interface Notification {
id, type, title, content, data, isRead, readTime, createTime
}
// 权限申请
interface Apply {
id, type, applicantId, applicantName, applicantAvatar,
targetId, targetName, reason, status, reviewerId,
reviewerName, reviewTime, reviewRemark, createTime
}
```
### 扩展的 API
```typescript
// 通知相关
pageNotification, getUnreadCount, markAsRead, markAllAsRead, deleteNotification
// 申请相关
pageApply, pageMyApply, createApply, reviewApply
```
### 路由配置更新
新增 20+ 个页面路由
---
## Phase 3 开发完成 ✅ (2026-04-12 20:15)
### 新增功能模块
#### 1. CI/CD 流水线
- **构建任务列表**:状态筛选/触发构建/取消构建/查看详情
- **构建详情**:构建信息/Git 信息/构建日志/构建产物
- **部署列表**:环境筛选/部署统计/触发部署/回滚操作
- **部署详情**:部署进度/日志/回滚功能
- **流水线配置**:启用/禁用、自动部署、分支规则、环境变量、Webhook
#### 2. 运营监控/数据分析
- **实时数据看板**:UV/PV/API 调用/错误数/Live 指示器
- **今日概览**:UV/PV/API 调用统计卡片
- **数据趋势**:图表占位(可集成 ECharts)
- **性能指标**:核心指标展示/趋势标签/错误统计
- **用户分析**:活跃用户/新增用户/留存率/地域分布/流量来源
- **页面排行**:PV/UV 排行
#### 3. 发票管理
- **发票列表**:状态筛选/发票详情/取消申请
- **可开票金额**:实时金额展示
- **申请发票**:抬头选择/金额输入/提交申请
- **发票抬头管理**:增删改查/默认设置
- **发票统计**:申请总数/已开票数量
#### 4. SSO 单点登录
- **SSO 配置**:OIDC/SAML/CAS/LDAP/OAuth2 提供商支持
- **连接配置**:Issuer/Client ID/Client Secret/授权 URL 等
- **用户同步**:同步开关/自动创建/默认角色
- **服务提供商信息**:Entity ID/ACS URL(可复制)
- **操作日志**:登录/登出/错误记录
### 新增类型定义
```
src/types/
├── index.ts # 统一导出
├── cicd.ts # CI/CD 流水线类型
├── analytics.ts # 数据分析类型
├── invoice.ts # 发票管理类型
└── sso.ts # SSO 单点登录类型
```
### 新增 API
```
src/api/
├── cicd.ts # CI/CD 流水线 API
├── analytics.ts # 数据分析 API
├── invoice.ts # 发票管理 API
└── sso.ts # SSO 单点登录 API
```
### 新增页面
```
src/developer/app/[id]/
├── builds.tsx # 构建列表
├── builds.scss
├── build/[id].tsx # 构建详情
├── build/[id].scss
├── deploys.tsx # 部署列表
├── deploys.scss
├── deploy/[id].tsx # 部署详情
├── deploy/[id].scss
├── pipeline.tsx # 流水线配置
├── pipeline.scss
├── analytics.tsx # 运营监控
└── analytics.scss
src/enterprise/
├── orders/
│ ├── invoice.tsx # 发票管理
│ └── invoice.scss
└── settings/
├── sso.tsx # SSO 配置
└── sso.scss
```
### Phase 3 完成的功能
| 功能 | 页面 | 状态 |
|------|------|------|
| **CI/CD 构建** | 构建列表/详情 | ✅ 完整 |
| **CI/CD 部署** | 部署列表/详情/回滚 | ✅ 完整 |
| **流水线配置** | 流水线设置 | ✅ 完整 |
| **运营监控** | 数据看板/趋势 | ✅ 完整 |
| **发票管理** | 发票列表/申请/抬头 | ✅ 完整 |
| **SSO 单点登录** | SSO 配置/日志 | ✅ 完整 |
---
### 待开发 (Phase 4)
| 功能 | 说明 |
|------|------|
| SDK 下载 | 各语言 SDK 下载页面 |
| 工单系统 | 客服工单/技术支持 |
| 账单导出 | 账单 Excel 导出 |
| 数据导入 | 企业数据批量导入 |
---
## Phase 4 开发完成 ✅ (2026-04-12 20:25)
### 新增功能模块
#### 1. SDK 下载中心
- **SDK 分类浏览**:Web/移动/服务端/小程序
- **SDK 列表展示**:图标/版本/下载量/Star
- **搜索功能**:按名称/描述搜索
- **SDK 详情弹窗**:版本/依赖/更新日志
- **一键安装**:NPM 命令复制/直接下载
#### 2. 工单系统
- **工单列表**:状态/类型/优先级筛选
- **工单统计**:总数/待处理/处理中/已解决
- **创建工单**:类型/优先级/标题/内容
- **工单详情**:沟通记录/解决方案/回复
- **工单评价**:满意度评分/反馈
#### 3. FAQ 常见问题
- **分类浏览**:API/账单/账户/SDK 等分类
- **搜索功能**:关键词搜索
- **展开查看**:问题详情/回答
- **有帮助/没帮助**:用户反馈
#### 4. 数据导入
- **导入记录列表**:状态/进度/错误统计
- **模板下载**:各类数据导入模板
- **字段说明**:必填/类型/示例
- **错误详情**:行号/字段/错误原因/建议
#### 5. 账单导出
- **导出记录**:任务列表/状态/文件信息
- **新建导出**:类型/格式/时间范围
- **下载管理**:文件下载/过期提醒
- **格式支持**:Excel/CSV/PDF
### 新增类型定义
```
src/types/
├── sdk.ts # SDK 下载类型
├── ticket.ts # 工单系统类型
└── import.ts # 导入导出类型
```
### 新增 API
```
src/api/
├── sdk.ts # SDK 下载 API
├── ticket.ts # 工单系统 API
└── import.ts # 导入导出 API
```
### 新增页面
```
src/developer/
├── sdk/
│ └── index.tsx # SDK 下载中心
├── ticket/
│ ├── index.tsx # 工单中心
│ ├── detail.tsx # 工单详情
│ └── faq.tsx # FAQ 常见问题
src/enterprise/
├── settings/
│ └── import.tsx # 数据导入
└── orders/
└── bills.tsx # 账单导出
```
### Phase 4 完成的功能
| 功能 | 页面 | 状态 |
|------|------|------|
| **SDK 下载中心** | 列表/详情/下载 | ✅ 完整 |
| **工单系统** | 列表/创建/详情/评价 | ✅ 完整 |
| **FAQ 常见问题** | 分类/搜索/反馈 | ✅ 完整 |
| **数据导入** | 导入记录/模板下载 | ✅ 完整 |
| **账单导出** | 导出记录/新建导出 | ✅ 完整 |
---
## 📊 四个 Phase 累计完成
| Phase | 功能数 | 页面数 | API 数 |
|-------|--------|--------|--------|
| Phase 1 | 基础框架 | 15+ | 10+ |
| Phase 2 | API Key/成员/通知等 | 20+ | 15+ |
| Phase 3 | CI-CD/监控/发票/SSO | 10+ | 20+ |
| Phase 4 | SDK/工单/导入/导出 | 10+ | 15+ |
| **合计** | - | **55+** | **60+** |
---
## Bug 修复 (2026-04-12 21:00)
### 已修复的问题
#### 1. JSX 语法错误
- 修复 8 个被截断的 `.tsx` 文件(appCredential、appEvent、appUser、appVersion)
- 修复 `user/apps/index.tsx` 中的 `>` token 问题
#### 2. API 导入问题
- 修复 `analytics.ts`、`cicd.ts`、`invoice.ts`、`sso.ts` 的 request 导入
- 添加缺失的 `ImportType` 导入
- 修复 `sdk.ts` 未使用的导入
#### 3. Config 文件错误
- 修复所有 `defineComponentConfig` → `definePageConfig`
- 涉及 12 个 config 文件
#### 4. 组件命名错误
- 修复 `PullRefresh` → `PullToRefresh`(nutui 组件)
- 涉及 4 个文件
#### 5. taro-ui 依赖问题
- 将 `taro-ui` 替换为 `nutui-react-taro`
- 重写 `invoice.tsx`、`sso.tsx` 页面
#### 6. SCSS 导入问题
- 修复 `notification/index.tsx` 的 scss 导入路径
#### 7. 类型定义
- 创建 `src/types/developer.ts` 缺失类型文件
- 修复 `RequestConfig` 缺少 `params` 属性
### 项目状态
- ✅ 编译成功
- ✅ 开发服务器运行中 (端口: dist)
---
## 任务:改造 invite/index.tsx 登录流程
### 需求
- `loginByOpenId` 已注册 → 显示**「确认加入」**按钮
- `loginByOpenId` 未注册 → 显示**「微信手机号快速加入」**按钮(走授权流程)
### 关键逻辑
1. `checkLoginStatus`:调用 `loginByOpenId` 检查用户是否已注册
2. 已注册:`isLoggedIn = true`,显示「确认加入」按钮
3. 未注册:`isLoggedIn = false`,显示「微信手机号授权」按钮
4. 授权成功 → 调用 `loginByMpWxPhone` 完成注册/登录 → 自动执行加入应用
### 文件修改
- `src/passport/invite/index.tsx` - 完整重写,区分已登录/未注册两种按钮状态
---
## 任务:未注册用户在邀请页内完成授权注册,不跳登录页
### 需求
- loginByOpenId 未注册 → 在页面内显示「微信手机号授权」按钮
- 授权成功 → 调用 `loginByMpWxPhone` 注册/登录 → 自动执行加入应用
- 不再跳转 passport/login 页面
### 关键逻辑
1. `checkLoginStatus`:已注册 isLoggedIn=true,未注册 isLoggedIn=false,**两种情况都显示邀请页**
2. 未注册按钮:`open-type="getPhoneNumber"` → `handleGetPhoneNumber`
- 授权码调 `SERVER_API_URL/wx-login/loginByMpWxPhone` 完成注册登录
- 保存 token → isLoggedIn=true → 立即调 `doJoinApp`
3. 已注册按钮:普通 `onClick` → `handleConfirmJoin` → `doJoinApp(access_token)`
4. `doJoinApp`:统一加入接口,请求头带 `Authorization: Bearer {access_token}`
### 文件修改
- `src/passport/invite/index.tsx` - 完整重写(彻底移除跳登录页逻辑)
---
## 修复:「授权码不能为空」报错
### 问题
后端 `/api/_app/developer/invite/accept` 接口强制要求传 `code`(微信授权码),不传就报「授权码不能为空」。
### 解决
统一用一个 `getPhoneNumber` 按钮处理两种场景:
- **已注册**:文字「确认加入」→ 触发 getPhoneNumber → `doJoinApp(code, accessToken)`
- **未注册**:文字「微信手机号快速加入」→ 触发 getPhoneNumber → 先 `loginByMpWxPhone` 注册 → 再 `wx.login()` 获 code → `doJoinApp(newCode, access_token)`
### doJoinApp 参数
```ts
doJoinApp(wxCode: string, accessToken: string)
// 请求体带 code,请求头带 Authorization: Bearer xxx
```
---
## 优化:已登录用户不弹手机号授权
### 改动
- 已登录按钮:普通 `onClick`,文字「确认加入」
- 未注册按钮:`getPhoneNumber` 授权,文字「微信手机号快速加入」
### 逻辑差异
| 用户状态 | 按钮类型 | 获取 code 方式 |
|------|------|------|
| 已登录 | 普通 onClick | `wx.login()` |
| 未注册 | getPhoneNumber | 授权回调的 `code` |
### 文件修改
- `src/passport/invite/index.tsx` - 按钮区分两种类型,已登录用普通 onClick
---
## 优化:已登录用户不强制勾选协议
### 改动
- 已登录用户点击「确认加入」时,不再检查 `agreementChecked`
- 未注册用户仍需勾选协议后才能授权手机号
### 文件修改
- `src/passport/invite/index.tsx` - `handleConfirmJoin` 移除协议检查
---
## 修复:后端需要手机号授权码
### 问题
后端 `invite/accept` 接口只接受 `getPhoneNumber` 获取的手机号授权码,用 `wx.login()` 的 code 会报「获取手机号失败」。
### 解决
两种用户状态都走 `getPhoneNumber` 按钮:
- 已登录:文字「确认加入」,回调 `handleConfirmJoinGetPhoneNumber`
- 未注册:文字「微信手机号快速加入」,回调 `handleGetPhoneNumber`
### 差异
| 用户状态 | 回调 | 行为 |
|------|------|------|
| 已登录 | `handleConfirmJoinGetPhoneNumber` | 直接用 `code + access_token` 调加入接口 |
| 未注册 | `handleGetPhoneNumber` | 先 `loginByMpWxPhone` 注册登录,再调加入接口 |
### 文件修改
- `src/passport/invite/index.tsx` - 两种状态都用 getPhoneNumber 按钮
---
## 任务:后端改造支持已登录用户直接加入
### 问题
后端 `/api/_app/developer/invite/accept` 接口强制要求传 `code`(手机号授权码),导致已登录用户也需要弹手机号授权。
### 后端改造方案
修改 `AppMpInviteController.acceptInvite` 方法:
#### 1. 参数校验调整
- `code` 改为可选参数
- 不传 `code` 时,从 `Authorization` 头获取当前登录用户
#### 2. 双模式支持
```java
if (StrUtil.isBlank(code)) {
// 模式一:已登录用户(通过 Authorization 头识别)
userId = getCurrentUserId();
} else {
// 模式二:未注册用户(通过手机号授权码获取手机号,创建用户)
String phone = getPhoneByCode(code);
userId = getOrCreateUserByPhone(phone);
}
```
#### 3. getCurrentUserId 方法
- 尝试从 Spring Security Context 获取
- 如果获取不到(免登录接口),手动解析 `Authorization` 头的 JWT Token
### 前端配合改造
- 已登录用户:普通 `onClick` 按钮 → `handleConfirmJoin` → `doJoinAppForLoggedInUser`(不传 `code`)
- 未注册用户:`getPhoneNumber` 按钮 → `handleGetPhoneNumber` → `doJoinAppForNewUser`(传 `code`)
### 文件修改
**后端:**
- `/Users/gxwebsoft/JAVA/websopy-java/src/main/java/com/gxwebsoft/app/controller/AppMpInviteController.java`
- `acceptInvite` 方法支持 `code` 可选
- 使用 `BaseController.getLoginUserId()` 获取当前登录用户(无需额外方法)
**前端:**
- `/Users/gxwebsoft/VUE/websopy-mp/src/passport/invite/index.tsx`
- 已登录按钮改为普通 `onClick`
- 新增 `handleConfirmJoin` 方法
- 拆分 `doJoinApp` 为 `doJoinAppForLoggedInUser` 和 `doJoinAppForNewUser`
---
## 修复:开发者中心加载不到应用
### 问题
用户通过邀请加入应用后,开发者中心显示「加载中...」,无法显示应用列表。
### 原因
- 前端 `developer/index.tsx` 只调用了 `pageMyApp` 接口(查询用户**创建**的应用)
- 用户通过邀请加入的应用属于**参与**的应用,不是创建的应用
- 后端 `loginByOpenId` 返回了应用列表,但前端没有使用这个数据
### 解决
1. **前端改造**:`loadData` 同时调用两个接口:
- `pageMyApp` - 查询创建的应用
- `pageJoinedApp` - 查询参与的应用(新增 API)
- 合并两个列表,根据 `productId` 去重
2. **新增 API**:`src/api/developer/developer.ts` 添加 `pageJoinedApp` 方法
### 文件修改
- `src/developer/index.tsx` - `loadData` 同时查询创建和参与的应用
- `src/api/developer/developer.ts` - 新增 `pageJoinedApp` 方法
-77
View File
@@ -1,77 +0,0 @@
# 2026-04-13 工作日志
## 完成的工作
### 1. 阿里云实人认证接入
**目的**:为 user/userVerify 页面添加阿里云身份证二要素核验
**后端改动**(JAVA/websopy-java):
- pom.xml:添加 `com.aliyun:cloudauth20190307:2.2.4` 依赖
- application.yml:添加 `cloudauth` 配置项(accessKeyId、accessKeySecret、endpoint、regionId)
- 新增 `CloudAuthProperties.java`:配置属性类
- 新增 `IdVerificationService.java`:实人认证服务
- 新增 `IdVerificationController.java`:实人认证 API 控制器
**前端改动**(VUE/websopy-mp):
- api/system/userVerify/index.ts:添加 `verifyIdCard()` API 调用
- user/userVerify/index.tsx:
- 导入 verifyIdCard
- 修改 submitSucceed 函数:个人认证时先调用实名校验,核验通过后再提交
### 2. 调研结论
- **城市服务实名校验**:已于2021年11月停止开放,不可用
- **阿里云实人认证**:推荐方案,0.2元/次,有100次免费试用额度
- **接入方式**:身份证二要素核验(姓名+身份证号)最简单
## 待办事项
- [x] 配置阿里云 AccessKey(在 application-prod.yml 中设置 cloudauth.accessKeyId 和 cloudauth.accessKeySecret)
- [ ] 在阿里云实人认证控制台开通服务并充值
- [ ] 测试验证接口是否正常工作
## 阿里云 AccessKey 配置
- 项目:websopy-java
- 文件:application-prod.yml
- accessKeyId: LTAI4GKGZ9Z2Z8JZ77c3GNZP
- 备注:与 OSS 使用同一个 AccessKey
## 3. 前端接口地址修正
**目的**:修正小程序端开发者相关 API 接口地址,与后端 Controller 路径保持一致
**问题发现**:`BaseUrl` 配置已包含 `/api` 后缀 (`https://websopy-api.websoft.top/api`),前端代码中不应再重复添加 `/api` 前缀,否则会导致 `/api/api/` 路径错误。
**修正文件**:
- `src/api/developer/enterprise.ts`:
- 企业信息:`/system/tenant/info`, `/system/tenant`
- 企业成员:`/system/user/page`, `/system/user`
- 订单:`/system/order/page`, `/system/order`
- 账单:`/sys/recharge-order/page`
- 企业应用:`/app/product/page`
- 邀请:`/app/developer/invite`
- `src/api/developer/developer.ts`:
- 项目/应用:`/app/product/*` (create, update, delete, detail, page, my/page, joined/page)
- API Key:`/app/app-credential/*`
- 版本发布:`/app/app-version/*`
- 开发者信息:`/app/developer/git-account`, `/app/developer/gitea-info`
- 通知:`/app/notification/*`
- 权限申请:`/app/developer/permission-requests/*`
- `src/api/invite/index.ts`:
- 所有接口移除 `/api` 前缀
**后端 Controller 对应关系**:
| 前端 API | 后端 Controller | 路径前缀 |
|---------|----------------|---------|
| enterprise.ts | TenantController | /api/system/tenant |
| enterprise.ts | UserController | /api/system/user |
| enterprise.ts | OrderController | /api/system/order |
| enterprise.ts | RechargeOrderController | /api/sys/recharge-order |
| developer.ts | AppProductController | /api/app/product |
| developer.ts | AppCredentialController | /api/app/app-credential |
| developer.ts | AppVersionController | /api/app/app-version |
| developer.ts | GitAccountController | /api/app/developer |
| developer.ts | AppNotificationController | /api/app/notification |
| developer.ts | AppPermissionRequestController | /api/app/developer/permission-requests |
| invite/index.ts | - | /api/invite/* |
**重要配置**:`config/env.ts` 中 `API_BASE_URL` 已包含 `/api` 后缀,前端代码路径不应再以 `/api` 开头。
-61
View File
@@ -1,61 +0,0 @@
## 修复 WXSS 不兼容的 .\!visible CSS 选择器
- **问题**: Tailwind JIT 引擎把 JS 代码 `if (!visible)` 误识别为 `!visible` important 修饰符类名,生成 `.\!visible` CSS 选择器,WXSS 不支持反斜杠转义的 `!`
- **修复**: 创建 PostCSS 插件 `postcss-remove-tailwind-important.js` 移除所有 `.\!xxx` 选择器
- **配置**: 更新 `postcss.config.js` 引入该插件(使用函数引用方式,非字符串模块名)
- **验证**: 构建通过,`.\!visible` 从输出中移除,`.visible` 保留
- **注意**: 项目中实际无人使用 Tailwind 的 `!` 修饰符类名,安全移除。将来若需使用,需创建对应 WXSS 兼容类名
## 替换项目中不兼容 WXSS 的 space-x/y-* 类名
- **问题**: `space-y-*` / `space-x-*` 依赖 `:not()` + `~` + CSS 变量 + 嵌套 calc(),WXSS 不兼容
- **修复**:
- 源码中将所有 `space-y-*` 替换为 `flex flex-col gap-*`
- 注释中的 `space-y-*` 也一并替换,防止 Tailwind JIT 误扫描生成
- 扩展 PostCSS 插件为 `postcss-remove-wxss-incompatible.js`,一并移除 `space-*` 和 `divide-*` 规则
- 删除旧的 `postcss-remove-tailwind-important.js` 文件
- **涉及文件**: dealer/qrcode/index.tsx, dealer/invite-stats/index.tsx, components/QRScanModal.tsx
- **验证**: 构建通过,所有不兼容选择器已从输出中移除
## 审查并修复剪切板接口合规问题
- **问题**: 用户截图显示「复制成功」与系统「内容已复制」重复 Toast,担心违反 5.15.4 剪切板接口滥用规范
- **审查结果**:
- 所有复制操作均为用户点击触发,无 `getClipboardData` 自动读取
- 无复制后强制跳转或中断业务流程
- 当前实现基本合规,主要问题是重复 Toast 影响体验
- **修复**: 更新 `src/utils/common.ts` 中的 `copyText` 函数,小程序端不再显示自定义「复制成功」Toast,由系统自带提示替代
- **验证**: weapp 构建通过
## 修复首页「复制失败」Toast 问题
- **问题**: 微信审核反馈小程序打开首页提示「复制失败」,截图显示该 Toast 来自产品矩阵页面
- **分析**:
- 首页无生命周期自动复制,复制均由用户点击触发
- 「立即开通」按钮点击后通过 `copyText` 复制外链,`copyText` 的 `fail` 回调会弹「复制失败」Toast
- 部分机型/场景下 `Taro.setClipboardData` 可能误报失败,导致审核不通过
- **修复**:
- 更新 `src/utils/common.ts` 的 `copyText`:小程序端直接调用原生 `wx.setClipboardData`,失败时只 `console.error` 不弹 Toast
- 优化 `src/pages/index/index.tsx` 的 `openMaybeLink`:相对路径改为 `Taro.navigateTo` 跳转,仅外链复制
- 「了解模板/插件市场」按钮不再调用 `openMaybeLink('/market')`,仅保留提示 Toast
- **验证**: weapp 构建通过,dist 中不再包含「复制失败」字符串
## 修复 wx 全局变量 TypeScript 红色警告
- **问题**: `src/utils/common.ts` 中使用 `wx.setClipboardData` 时,TypeScript 报「Cannot find name 'wx'」红色警告
- **原因**: 项目没有声明微信小程序原生全局对象 `wx` 的类型
- **修复**: 在 `types/global.d.ts` 中添加 `declare const wx: any;` 全局类型声明
- **验证**: `tsc` 不再报 `common.ts` 相关错误,`npx taro build --type weapp` 构建成功
## 去掉首页「复制咨询模板」按钮及复制逻辑
- **问题**: `wx.setClipboardData` 在小程序开发者工具中报 `no permission` 错误,且审核对复制功能敏感
- **修复**:
- 移除 `src/pages/index/index.tsx` 中的 `copyConsultTemplate` 函数
- 删除「复制咨询模板」按钮,只保留「电话咨询」
- 修改联系我们区域文案,去掉「复制咨询模板」相关描述
- `openMaybeLink` 中外链不再调用 `copyText`,改为 `Taro.showModal` 提示用户手动复制到浏览器打开
- 移除首页对 `copyText` 的 import
- **验证**: `npx taro build --type weapp` 构建成功,首页无复制相关调用
- **备注**: 项目其他页面仍有复制功能(如礼品卡分享、优惠券分享、订单号复制、密钥复制等),如需要可继续逐个移除
-15
View File
@@ -1,15 +0,0 @@
# 2026-07-01 工作日志
## 修复支付页面 request URL 无效问题
- **文件**: `src/utils/request.ts`
- **问题**: 扫码进入支付页后报错 `request:fail invalid url "/app/subscription/detail-by-no/..."`,原因是核心 `request()` 函数没有调用 `buildUrl()` 拼接 baseUrl,相对路径直接传给微信 `wx.request()` 导致失败
- **修复**: 在 `request()` 函数中统一加 `buildUrl(options.url)` 处理,确保所有请求方式都能正确拼接完整 URL
## 小程序码改为体验版
- **文件**: `src/api/invite/index.ts`(前端)
- **修改**: `generateMiniProgramCode` 函数将 `envVersion` 作为查询参数传给后端,默认值 `'trial'`
- **后端配合**:
- `WxLoginController.java`: `getOrderQRCodeUnlimited` 和 `getOrderQRCode` 方法新增 `@RequestParam(defaultValue = "release") String envVersion` 参数
- 硬编码的 `"release"` 改为动态读取 `envVersion` 参数
- `getQRCodeText` 已有此参数,无需修改
- **注意**: 提审上线前需将前端默认 `envVersion` 改回 `'release'`
-101
View File
@@ -1,101 +0,0 @@
# 2026-07-02 工作日志
## 小程序扫码支付"获取订单失败"问题排查与修复
- **问题**:用户扫小程序码后进入小程序支付页,提示"获取订单失败"
- **根因**:后端 Spring Security 的 SecurityConfig 中未将支付相关接口加入白名单(permitAll),扫码进入时用户未登录态,请求 `/api/app/subscription/detail-by-no/**` 被 401 拦截
- **修复**:在 SecurityConfig.java 的 antMatchers 中添加了以下接口白名单:
- `/api/app/subscription/detail-by-no/**` — 根据订阅号查询订单详情
- `/api/app/subscription/mp-prepay/**` — 创建小程序预支付订单
- `/api/app/subscription/mp-confirm/**` — 支付成功确认
- `/api/app/subscription/wx-notify/**` — 微信支付回调通知
- `/api/wx-login/**` — 小程序码生成等微信登录相关接口(之前只覆盖了 `/api/shop/wx-login/**`,漏掉了 `/api/wx-login/**`)
- **前端代码位置**:`src/passport/pay/index.tsx`,支付页面从 scene/subscriptionNo 获取订单号,调 detail-by-no 接口
- **后端代码位置**:`AppSubscriptionController.java`(@RequestMapping("/api/app/subscription")),`WxLoginController.java`(@RequestMapping("/api/wx-login"))
- **注意**:生成小程序码时 `envVersion` 默认为 `trial`(体验版),上线前需改为 `release`
### 后续排查:后端白名单加了仍报"获取订单失败"
- **二次根因**:前端 `src/passport/pay/index.tsx` 的 `fetchDetail` 存在逻辑 bug
- `request()` 默认 `returnRaw=false`,响应拦截器对 `code===0` 的响应已自动拆包,只返回 `data` 部分(订阅对象)
- 但 `fetchDetail` 又检查 `res?.code === 200 || res?.code === 0`,而拆包后的 `res`(订阅对象)没有 `code` 字段
- 因此条件永远为 `false`,走进 `setErrorMsg` 分支,显示"获取订单失败"
- **修复**:移除多余的 `code` 检查,`request` 成功即代表业务成功,直接使用返回的 `data` 对象;错误由 catch 处理
- **附加**:`getSubscriptionNo` 加了 `console.log` 打印页面参数和解析结果,方便真机调试排查 scene 传入问题
### 五次排查:getOpenId 返回 success:true 但 openid 为 undefined
- **根因**:前端 `src/api/passport/wx-login/index.ts` 的 `getOpenId` 函数错误地假设后端返回的是 `{ openid, unionid, session_key }` 对象,直接取 `res.data.openid`;但后端 `/wx-login/getOpenId` 实际返回的是 `LoginResult` 结构 `{ access_token, user }`,真正的 openid 在 `user.openid` 里
- **修复**:更新 `getOpenId` 类型为 `ApiResult<WxLoginResult>`,从 `res.data.user?.openid` 提取 openid,同时把 `access_token` 和 `user` 一起返回给调用方复用
- **后续影响**:`handlePay` 中拿到 openid 后,因为 `getOpenId` 已经自动注册/登录(后端会注册新用户并签发 token),理论上用户已经自动登录,可能无需再调 `loginByOpenId`;但当前代码保留双保险
- **根因**:`handlePay` 流程有 bug——调了 `Taro.login()` 获取 code,但没有用 code 去换 openid,直接从 `Taro.getStorageSync('openid')` 读取(扫码用户可能没登录过,storage 里没存)
- **前端修复**(`src/passport/pay/index.tsx`):
1. 引入 `getOpenId` 和 `loginByOpenId` API
2. `handlePay` 中:`Taro.login()` → `getOpenId(code)` 换取 openid → 缓存 openid
3. 尝试 `loginByOpenId` 自动登录获取 token(如果用户未注册,弹出引导登录对话框)
4. `mp-prepay` 请求也加了 `returnRaw: true` 和调试日志
- **后端修复**(`AppSubscriptionController.java`):
1. `mp-prepay`:将 `userId == null || !userId.equals(sub.getUserId())` 改为 `userId != null && !userId.equals(sub.getUserId())`,允许未登录用户发起支付
2. `mp-confirm`:同理,未登录时跳过 userId 校验,用 `effectiveUserId = userId != null ? userId : sub.getUserId()` 替代原 `userId`
- **修改点1**:`src/passport/pay/index.tsx` 的 `fetchDetail` 改为 `returnRaw: true`,拿到接口完整响应(含 code/message/data),手动判断业务状态码(`code === 0 || code === 200`),不再依赖拦截器拆包
- **修改点2**:`fetchDetail` 和 `getSubscriptionNo` 加了详细 console.log/console.error,打印:
- 原始页面参数(scene、subscriptionNo)
- 请求 URL 和当前 token
- 接口完整返回 JSON
- catch 中的 err.name/type/code/message/data
- **修改点3**:`src/utils/request.ts` 响应拦截器去掉了 `process.env.NODE_ENV === 'development'` 条件限制,所有环境都打印日志,方便真机调试 Console 查看
### 六次排查:getOpenId 返回 success:true 但 openid 为 null
- **现象**:前端 `getOpenId` 返回 `success: true, access_token: 有值, user.userId: 35619`,但 `openid: null, unionid: null`
- **根因**:后端 `/wx-login/getOpenId` 虽然通过微信 `jscode2session` 换到了 openid,但**没有把 openid 设置到 `UserParam` 中**。
- 后续 `userService.getByOauthId(userParam)` 查找用户时,userParam.openid 为空,可能无法命中已有用户;
- 新用户注册时走 `addUser(userParam)`,而 `addUser` 只在 `userParam.openid` 非空时才会写 `User.openid`,所以新用户 openid 也为空;
- 对于已存在但 openid 为空的老用户(如手机号注册),直接返回,openid 仍然是 null。
- **修复**(`WxLoginController.java` 的 `/getOpenId` 方法):
1. 从微信返回中拿到 `openid`/`unionid` 后,立即 `userParam.setOpenid(openid)` 和 `userParam.setUnionid(unionid)`;
2. 用户已存在但 `user.openid` 为空时,把 openid/unionid 更新到数据库并返回最新 user;
3. 这样 `LoginResult.user.openid` 一定有值,前端就能正确获取并传给 `mp-prepay`。
### 七次排查:mp-prepay 报 SIGN_ERROR(签名错误)
- **现象**:前端调用 `/api/app/subscription/mp-prepay/{id}` 时,后端返回 `code: 1, message: "微信支付服务异常: 微信错误码: SIGN_ERROR, 签名错误"`。
- **排查**:
1. 当前小程序 JSAPI 支付使用 `WxNativePayUtil.getConfig` 构建微信支付 Config,模式为 `RSAPublicKeyConfig`(公钥模式),但生产环境 `wechatpay-public-key-id` 为空。
2. 读取项目下所有证书文件,用 openssl 检查各证书对应的商户号,发现:
- `/wechat/websopy/apiclient_cert.pem`:商户号 `1557418831`(与小程序无关联)
- `/wechat/10398/apiclient_cert.pem`:商户号 `1246610101`(与小程序已关联,见截图)
3. 配置中的 `mch-id` 是 `1246610101`,但配置的 `private-key-relative-path` 指向 `wechat/websopy/`,导致证书与商户号不匹配,微信返回 `SIGN_ERROR`。
4. 检查 `10398` 目录证书与私钥匹配性:公钥哈希一致,证书序列号为 `48749613B40AA8F1D768583FC352358E13EB5AF0`。
- **修复**:
1. 修改 `WxNativePayUtil.java`:将 `RSAPublicKeyConfig`(公钥模式)改为 `RSAAutoCertificateConfig`(自动证书模式),不再依赖 `wechatpay-public-key-id` 和 `wechatpay-cert-relative-path`。
2. 修正 `application-dev.yml` 和 `application-prod.yml`:
- `mch-id`:`1246610101`(保持不变,这是正确的)
- `merchant-serial-number`:`48749613B40AA8F1D768583FC352358E13EB5AF0`(对应 10398 目录证书)
- `private-key-relative-path`:`wechat/10398/apiclient_key.pem`
- `wechatpay-cert-relative-path` 置空(自动证书模式)
- **待确认**:当前保留的 `api-v3-key: "zGufUcqa7ovgxRL0kF5OlPr482EZwtn9"` 是否属于商户号 `1246610101`,需要用户在微信支付商户后台确认。如果仍报签名错误,需要替换为正确的 APIv3 密钥。
- **待处理**:生产环境需将 `/Users/gxwebsoft/JAVA/websopy-java/src/main/resources/wechat/10398/apiclient_key.pem` 上传到 `/www/wwwroot/file.ws/wechat/10398/apiclient_key.pem`,并确保该文件可被 Java 进程读取。之前错误上传 `wechat/websopy/` 目录的证书给 1246610101 使用,这是 `SIGN_ERROR` 的真正原因。
### 八次排查:SUB202607022056385942 提示"订阅不存在"
- **现象**:调用 `/api/_app/subscription/check-status/SUB202607022056385942` 和 `/api/_app/subscription/detail-by-no/SUB202607022056385942` 均返回"订阅不存在"
- **根因**:`getOrderQRCodeUnlimited` 接口(WxLoginController.java 第 441 行)**只生成小程序码,并不创建订阅记录**。如果生成小程序码时没有先调用 `subscribe` 接口,`subscriptionNo` 就只是个随机字符串,没有对应的数据库记录
- **修复(方案 1)**:新增 `POST /api/app/subscription/generate-pay-qrcode` 接口,在一个请求中完成:
1. 创建订阅记录(复用 `subscribe` 的核心逻辑,写入数据库)
2. 用 `subscriptionNo` 调用 `WxMiniprogramUtil.generateMiniprogramQrCode` 生成小程序码
3. 返回 `subscriptionNo` + Base64 图片
- **关联修改**:
- `AppPayProperties.java`:新增 `miniAppSecret` 字段(用于生成小程序码获取 access_token)
- `application-dev.yml` / `application-prod.yml`:添加 `mini-app-secret` 配置项(当前为占位符,需用户填写真实秘钥)
- `AppSubscriptionController.java` `mp-prepay`:已改为允许跨用户支付(`userId != null && !userId.equals(sub.getUserId())`)
- `AppSubscriptionController.java` `mp-confirm`:已改为未登录时跳过 userId 校验
- **下一步**:
1. 到微信公众平台获取小程序 `AppSecret`,填写到 `application-dev.yml` 和 `application-prod.yml` 的 `mini-app-secret` 字段
2. 重新构建部署后端
3. 调用新接口生成支付小程序码:`POST /api/app/subscription/generate-pay-qrcode` Body: `{"productId": 1, "subscriptionPeriod": "month", "envVersion": "trial"}`
4. 用返回的 `subscriptionNo` 测试 `check-status` 和 `detail-by-no` 接口,确认订阅记录已存在
### 补充修复
- `AppSubscriptionController.java` 的 `generate-pay-qrcode` 接口中,`switch (subscriptionPeriod)` 的 `case "month":` 空 fall through 触发 IDE 红色警告。已添加 `// fall through to default` 注释消除警告。
+29
View File
@@ -0,0 +1,29 @@
# 2026-07-16 工作日志
## passport 分包化修复
用户把登录/注册相关页面放入 `passport` 分包(`src/app.config.ts` 的 subpackages)。
排查结论:
- 主包内所有跳转引用(`src/pages/**`、`src/utils/auth.ts`、`src/components/QRLoginScanner.tsx` 等)已全部使用 `/passport/xxx` 路径。分包化前后 URL 路径不变,引用代码无需修改。
- passport 内 10 个页面(login/forget/setting/agreement/sms-login/register/qr-login/qr-confirm/unified-qr/pay)均有 `.tsx` + `.config.ts`,`@` 别名指向 `src/`,移动目录不影响 import。
修复 `src/app.config.ts` 分包声明(与实际文件对齐):
- 移除 `phone-auth/index`、`invite/index`、`webview/index`(文件不存在,且全局无任何跳转引用它们,会导致 Taro 编译报错)
- 补充 `register`(`src/passport/register.tsx` 存在但未声明)
- 统一引号风格、清理尾随逗号
## passport/pay 功能从 websopy-taro-bak 迁移
问题:`src/passport/pay/index.tsx` 失效,原因是当前项目基于 xinlong-shop 框架,`API_BASE_URL` 指向商城接口 `https://shop-api.websoft.top/api`,而 pay 页面需要访问 websopy 业务接口 `https://websopy-api.websoft.top/api` 的 `/app/subscription/*`。
迁移与修复:
- 确认 `src/passport/pay/` 与 `src/api/passport/wx-login/` 文件和备份项目一致,无需替换。
- 在 `config/env.js` 新增 `WEBSOPY_API_BASE_URL`。
- 在 `config/app.ts` 导出 `WebsopyBaseUrl`。
- 创建 `config/env.d.ts` 消除 `config/app.ts` 导入 `.js` 的隐式 any 类型错误。
- 增强 `src/utils/request.ts`:在 `RequestConfig` 增加 `baseUrl?: string`,`request`/`get`/`post` 等函数支持自定义 baseUrl,相对路径会自动拼接;完整 URL 保持不变。
- 修改 `src/passport/pay/index.tsx`:三个订阅接口请求均传入 `baseUrl: WebsopyBaseUrl`;修复 `tenantId` 类型(string -> number)和 `saveStorageByLoginUser` 的 user 类型断言。
- 运行 `npx taro build --type weapp` 构建成功;dist 中 `passport/pay` 文件生成,确认请求使用 websopy API。
- 说明:`npm run type-check` 仍因 `@tarojs/taro` 类型包解析问题失败,这是项目既有环境问题,不影响构建。
-52
View File
@@ -1,52 +0,0 @@
# websopy-taro 项目记忆
## Tailwind CSS 在小程序中的兼容性规则
以下 Tailwind 类名在 WXSS 中不兼容,项目内禁止使用,已通过 PostCSS 插件 `postcss-remove-wxss-incompatible.js` 自动移除对应的 CSS 规则:
1. **`!xxx` important 修饰符**(如 `!visible`):生成 `.!visible` 选择器,WXSS 不支持反斜杠转义的 `!`。
2. **带 `0.5` 的类名**(如 `w-0.5`、`h-0.5`、`gap-0.5`):类名中的 `.` 被转义为 `\.`,生成 `.w-0\.5` 这样的选择器,WXSS 不兼容。应改用等效的整数尺寸或自定义值替代。
3. **`space-x-*` / `space-y-*`**(如 `space-y-4`):依赖 `:not()` + `~` 兄弟组合器 + CSS 变量 + 嵌套 `calc()`,WXSS 全不支持。应改用 `flex flex-col gap-*` 实现等效间距。
4. **`divide-x-*` / `divide-y-*`**:同样依赖 `:not()` + `~`,WXSS 不兼容。
## 替代方案速查
| 禁用类名 | 替代写法 |
|---------|---------|
| `space-y-4` | `flex flex-col gap-4` |
| `space-x-4` | `flex flex-row gap-4` |
| `!visible` | 直接写 `style={{ visibility: 'visible' }}` 或自定义类 |
| `w-0.5` | 用自定义值或 `w-1` 替代 |
## 微信小程序接口合规
### 剪切板接口(运营规范 5.15.4)
1. **只允许用户主动触发复制**:所有 `Taro.setClipboardData` 调用必须绑定在明确的用户点击事件上,禁止在无用户操作时自动读写剪切板。
2. **禁止复制后强制中断流程**:复制成功后不得强制跳转、强制关注、强制添加客服等,不能要求用户通过其他方式才能完成当前业务流程。
3. **避免重复/失败 Toast 干扰体验**:微信小程序调用 `setClipboardData` 成功后系统会自带「内容已复制」提示。为避免重复提示及误报失败,统一封装 `utils/common.ts` 中的 `copyText` 函数:
- 小程序端(weapp)直接调用原生 `wx.setClipboardData`,不再额外显示自定义 `复制成功` / `复制失败` Toast
- 失败时仅通过 `console.error` 输出错误日志,便于排查
- 非小程序端保留原有成功/失败提示
### 项目内剪切板使用现状
已审查所有 `setClipboardData` 调用:
- 均通过按钮点击触发(复制咨询模板、复制密钥、复制链接、复制兑换码等)
- 无 `getClipboardData`(自动读取剪切板)调用
- 无复制后强制跳转或中断业务流程的行为
- 首页「立即开通」等外链按钮点击后复制链接;相对路径入口(如开发者中心)改为 `Taro.navigateTo` 跳转,不再复制
当前实现基本符合 5.15.4 规范。
## AppSubscription 状态字段语义(踩坑必读)
判断"是否已支付"必须用 `payStatus`(0=未支付 / 1=已支付)这个**支付状态**字段,**不能**用 `status`(订阅生命周期 active/pending/expired/cancelled)。
- `status`:订阅的生命周期状态,**不等于支付状态**。后端 `renewPay` 为了避免放弃支付导致服务中断,会故意保留原 `status='active'`、只把 `payStatus` 设为 0。
- `payStatus`:本次订单的支付状态。支付成功(mp-confirm / handleBalancePay)后才置 1。
续费场景下这两个字段完全解耦:用 `status === 'active'` 判支付会**直接误判为已支付**,详见 `2026-07-16.md` 修复记录。
-59
View File
@@ -1,59 +0,0 @@
# 配置说明文档
## 环境配置
### 1. 复制环境变量文件
```bash
cp .env.example .env
```
### 2. 修改配置文件
#### config/app.ts
```typescript
// 租户ID - 请根据实际情况修改
export const TenantId = 'YOUR_TENANT_ID';
// 接口地址 - 请根据实际情况修改
export const BaseUrl = 'https://your-api-domain.com/api';
```
#### src/utils/server.ts
```typescript
// 模版套餐ID - 请根据实际情况修改
export const TEMPLATE_ID = 'YOUR_TEMPLATE_ID';
// 服务接口 - 请根据实际情况修改
export const SERVER_API_URL = 'https://your-server-domain.com/api';
```
#### project.config.json
```json
{
"appid": "your_wechat_appid"
}
```
### 3. 小程序配置
#### 微信小程序
1. 在微信公众平台申请小程序
2. 获取AppID并填入 `project.config.json`
3. 配置服务器域名白名单
#### 支付宝小程序
1. 在支付宝开放平台申请小程序
2. 获取AppID并配置相应文件
#### 字节跳动小程序
1. 在字节跳动开发者平台申请小程序
2. 获取AppID并填入 `project.tt.json`
### 4. API配置
确保后端API服务正常运行,并配置正确的域名和端口。
### 5. 安全注意事项
- 不要将真实的AppID、API密钥等敏感信息提交到公开仓库
- 使用环境变量管理敏感配置
- 定期更新密码和密钥
- 在生产环境中启用HTTPS
-201
View File
@@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+198 -527
View File
@@ -1,592 +1,263 @@
<div align="center">
<h1>🚀 WebSoft 小程序</h1>
<p><strong>基于 Taro + React + TypeScript 的跨平台小程序应用</strong></p>
# Paopao Taro - 项目框架
<p>
<img src="https://img.shields.io/badge/Taro-4.0.8-blue" alt="Taro">
<img src="https://img.shields.io/badge/React-18.3.1-61DAFB" alt="React">
<img src="https://img.shields.io/badge/TypeScript-5.7.2-3178C6" alt="TypeScript">
<img src="https://img.shields.io/badge/NutUI-3.0.16-orange" alt="NutUI">
<img src="https://img.shields.io/badge/TailwindCSS-3.4.17-38B2AC" alt="TailwindCSS">
<img src="https://img.shields.io/badge/License-MIT-blue" alt="License">
</p>
</div>
基于 Taro 4.0.8 + React 18.3.1 + TypeScript 5.7.2 的跨端开发框架
## 📖 项目简介
WebSoft 小程序是一个基于 **Taro + React + TypeScript** 构建的现代化跨平台小程序应用,支持多端发布:
- **核心框架**:Taro 4.0.8 + React 18.3.1 + TypeScript 5.7.2
- **UI组件库**:NutUI React Taro 2.7.4 + NutUI Icons
- **样式方案**:Sass + TailwindCSS 3.4.17
- **状态管理**:React Hooks + Context API
- **工具库**:Day.js、Crypto-js、React Router DOM
## 🎯 支持平台
本项目基于 Taro 框架,支持一套代码多端运行:
- **微信小程序** (weapp)
- **支付宝小程序** (alipay)
- **百度智能小程序** (swan)
- **字节跳动小程序** (tt)
- **QQ小程序** (qq)
- **京东小程序** (jd)
- **H5网页** (h5)
- **React Native** (rn)
- **快应用** (quickapp)
## 项目演示
| 后台管理系统 | https://mp.websoft.top |
|--------|-------------------------------------------------------------------------------------------------------------------------------------|
| 演示账号 | 请联系开发者获取演示账号
| 正式账号 | [立即注册](https://mp.websoft.top/register/?inviteCode=github) |
| 关注公众号 | ![输入图片说明](https://oss.wsdns.cn/20240327/f1175cc5aae741d3af05484747270bd5.jpeg?x-oss-process=image/resize,m_fixed,w_150/quality,Q_90) |
## 🛠️ 技术栈
## 📦 技术栈
### 核心框架
| 技术 | 版本 | 说明 |
|------|------|------|
| Taro | 4.0.8 | 跨平台开发框架 |
| React | 18.3.1 | 前端UI框架 |
| TypeScript | 5.7.2 | 类型安全的JavaScript |
| Webpack | 5.78.0 | 模块打包工具 |
| Babel | 7.26.0 | JavaScript编译器 |
| PostCSS | 8.4.49 | CSS后处理器 |
| ESLint | 8.57.1 | 代码质量检查 |
- **Taro**: `4.0.8` - 跨端开发框架
- **React**: `18.3.1` - UI 框架
- **TypeScript**: `5.7.2` - 类型系统
### UI组件库
| 技术 | 版本 | 说明 |
|------|------|------|
| NutUI React Taro | 2.7.4 | 移动端组件库 |
| NutUI Icons | 2.0.1 | 图标库 |
| NutUI Biz | 1.0.0-beta.2 | 业务组件库 |
### UI 组件库
- **NutUI React Taro**: `2.7.4` - 京东风格的 Taro React 组件库
- **NutUI Icons**: `^1.0.0` - 图标库
### 样式方案
| 技术 | 版本 | 说明 |
|------|------|------|
| Sass | - | CSS预处理器 |
| TailwindCSS | 3.4.17 | 原子化CSS框架 |
| Autoprefixer | 10.4.20 | CSS自动前缀 |
- **Sass**: `^1.81.0` - CSS 预处理器
- **TailwindCSS**: `3.4.17` - 原子化 CSS 框架
### 功能组件
- **React Router DOM** - 路由管理
- **Day.js** - 日期时间处理
- **Crypto-js** - 加密解密工具
- **js-base64** - Base64编码解码
- **React Markdown** - Markdown渲染
- **ECharts Taro3 React** - 图表组件
### 状态管理
- **React Hooks** - 内置 Hooks
- **Context API** - 跨组件状态共享
## 📋 环境要求
### 基础环境
- 📦 **Node.js 16+**
- 📱 **微信开发者工具** (开发微信小程序)
- 🔧 **支付宝开发者工具** (开发支付宝小程序)
- 🌐 **现代浏览器** (开发H5应用)
### 包管理器
- **npm** / **yarn** / **pnpm** (推荐使用pnpm)
### 开发工具
- **推荐**:VS Code / WebStorm
- **插件**:ES7+ React/Redux/React-Native snippets、TypeScript Importer
### 工具库
- **Day.js**: `^1.11.13` - 日期处理
- **Crypto-js**: `^4.2.0` - 加密解密
- **React Router DOM**: `^6.28.0` - 路由管理
## 🚀 快速开始
### 1. 克隆项目
```bash
git clone https://github.com/websoft-top/mp-taro.git
cd mp-taro
```
### 安装依赖
### 2. 安装依赖
```bash
# 使用 pnpm (推荐)
pnpm install
# 或使用 npm
# 使用 npm
npm install
# 或使用 yarn
# 使用 yarn
yarn install
# 使用 pnpm
pnpm install
```
### 3. 配置开发环境
### 开发模式
#### 环境变量配置
项目支持多环境配置,通过 `config/env.ts` 文件管理不同环境的配置:
```typescript
// config/env.ts
export const ENV_CONFIG = {
// 开发环境
development: {
API_BASE_URL: 'http://localhost:3000/api',
APP_NAME: 'Taro App Dev',
DEBUG: 'true',
},
// 生产环境
production: {
API_BASE_URL: 'https://api.example.com',
APP_NAME: 'Taro App',
DEBUG: 'false',
},
// 测试环境
test: {
API_BASE_URL: 'https://test-api.example.com',
APP_NAME: 'Taro App Test',
DEBUG: 'true',
}
}
```
#### 应用配置
编辑 `config/app.ts` 文件,配置租户ID等信息:
```typescript
export const TenantId = 'your_tenant_id'
export const BaseUrl = API_BASE_URL // 自动从环境配置读取
```
### 4. 启动开发服务器
#### 微信小程序开发
```bash
# 开发环境(默认)
# 微信小程序
npm run dev:weapp
# 生产环境
NODE_ENV=production npm run dev:weapp
# 测试环境
NODE_ENV=test npm run dev:weapp
```
然后使用微信开发者工具打开 `dist` 目录
#### H5开发
```bash
# 启动H5开发模式
# H5
npm run dev:h5
```
访问 `http://localhost:10086` 查看H5版本
#### 其他平台
```bash
# 支付宝小程序
npm run dev:alipay
# 百度智能小程序
# 百度小程序
npm run dev:swan
# 字节跳动小程序
npm run dev:tt
# QQ小程序
# QQ 小程序
npm run dev:qq
# 京东小程序
npm run dev:jd
```
## ⚙️ 配置说明
### 项目配置
主要配置文件位于 `config/` 目录:
- `config/index.ts` - Taro主配置文件
- `config/dev.ts` - 开发环境配置
- `config/prod.ts` - 生产环境配置
- `config/env.ts` - 环境变量配置(新增)
- `config/app.ts` - 应用配置(API地址、租户ID等)
### 环境变量配置
项目支持多环境配置,在 `config/env.ts` 中管理:
```typescript
// 环境变量配置
export const ENV_CONFIG = {
development: {
API_BASE_URL: 'http://localhost:3000/api',
APP_NAME: 'Taro App Dev',
DEBUG: 'true',
},
production: {
API_BASE_URL: 'https://api.example.com',
APP_NAME: 'Taro App',
DEBUG: 'false',
},
test: {
API_BASE_URL: 'https://test-api.example.com',
APP_NAME: 'Taro App Test',
DEBUG: 'true',
}
}
// 获取当前环境配置
export function getEnvConfig() {
const env = process.env.NODE_ENV || 'development'
if (env === 'production') {
return ENV_CONFIG.production
} else if (env === 'test') {
return ENV_CONFIG.test
} else {
return ENV_CONFIG.development
}
}
// 导出环境变量
export const { API_BASE_URL, APP_NAME, DEBUG } = getEnvConfig()
```
### 应用配置
在 `config/app.ts` 中配置:
```typescript
import { API_BASE_URL } from './env'
// 租户ID
export const TenantId = 'your_tenant_id'
// API基础地址(自动从环境配置读取)
export const BaseUrl = API_BASE_URL
// 其他配置...
```
### 环境切换
通过设置 `NODE_ENV` 环境变量来切换不同环境:
### 生产构建
```bash
# 开发环境(默认)
npm run dev:weapp
# 生产环境
NODE_ENV=production npm run dev:weapp
# 测试环境
NODE_ENV=test npm run dev:weapp
```
## 🌍 环境变量详细说明
### 环境变量文件结构
```
config/
├── env.ts # 环境变量配置文件
├── app.ts # 应用配置(使用环境变量)
└── index.ts # Taro配置(注入环境变量)
```
### 支持的环境变量
| 变量名 | 说明 | 开发环境默认值 | 生产环境建议值 |
|--------|------|---------------|---------------|
| `API_BASE_URL` | API基础地址 | `http://localhost:3000/api` | `https://api.yourdomain.com/api` |
| `APP_NAME` | 应用名称 | `Taro App Dev` | `Your App Name` |
| `DEBUG` | 调试模式 | `true` | `false` |
### 环境变量使用方法
#### 1. 修改环境配置
编辑 `config/env.ts` 文件:
```typescript
export const ENV_CONFIG = {
development: {
API_BASE_URL: 'http://localhost:3000/api', // 修改为你的开发环境API地址
APP_NAME: 'My App Dev',
DEBUG: 'true',
},
production: {
API_BASE_URL: 'https://api.yourdomain.com/api', // 修改为你的生产环境API地址
APP_NAME: 'My App',
DEBUG: 'false',
}
}
```
#### 2. 在代码中使用环境变量
```typescript
// 在 config/app.ts 中
import { API_BASE_URL, APP_NAME, DEBUG } from './env'
export const BaseUrl = API_BASE_URL
export const AppName = APP_NAME
export const IsDebug = DEBUG === 'true'
// 在其他文件中使用
import { BaseUrl } from '@/config/app'
console.log('API地址:', BaseUrl)
```
#### 3. 运行时环境切换
```bash
# 开发环境(默认)
npm run dev:weapp
# 生产环境
NODE_ENV=production npm run dev:weapp
# 测试环境
NODE_ENV=test npm run dev:weapp
# 构建生产版本
NODE_ENV=production npm run build:weapp
```
### 环境变量最佳实践
1. **敏感信息处理**:不要在代码中硬编码敏感信息,使用环境变量管理
2. **环境隔离**:确保不同环境使用不同的API地址和配置
3. **默认值设置**:为所有环境变量提供合理的默认值
4. **类型安全**:使用TypeScript确保环境变量的类型安全
### 路径别名配置
项目已配置路径别名,可以使用以下方式导入:
```typescript
import Component from '@/components/Component'
import { request } from '@/utils/request'
import { useCart } from '@/hooks/useCart'
import config from '@/config/app'
```
### 样式配置
- **Sass**: 支持嵌套、变量、混入等特性
- **TailwindCSS**: 原子化CSS,配置文件为 `tailwind.config.js`
- **PostCSS**: 自动处理CSS兼容性,配置文件为 `postcss.config.js`
## 🎯 核心功能
### 🔐 用户认证与授权
- **微信登录**:基于微信OpenID的用户认证
- **短信登录**:支持手机号短信验证登录
- **用户注册**:完整的用户注册流程
- **密码找回**:支持手机号找回密码
### 📝 内容管理系统(CMS)
- **文章浏览**:支持文章列表和详情查看
- **分类管理**:内容分类浏览
- **富文本渲染**:支持Markdown内容渲染
- **媒体展示**:图片、视频等媒体文件展示
### 🛒 电商系统
- **商品展示**:商品列表、详情、分类浏览
- **购物车**:商品加入购物车、数量管理
- **订单管理**:订单创建、查看、状态跟踪
- **地址管理**:收货地址的增删改查
- **支付集成**:支持微信支付等支付方式
### 👤 用户中心
- **个人资料**:用户信息查看和编辑
- **实名认证**:用户身份验证功能
- **帮助中心**:常见问题和帮助文档
- **关于我们**:应用介绍和联系方式
- **设置中心**:个人偏好设置
### 📊 数据可视化
- **图表展示**:基于ECharts的数据可视化
- **统计分析**:业务数据统计展示
- **实时更新**:动态数据更新
## 🏗️ 项目结构
```
src/
├── app.config.ts # 应用配置文件
├── app.scss # 全局样式
├── app.ts # 应用入口文件
├── api/ # API接口定义
│ ├── cms/ # 内容管理相关API
│ ├── shop/ # 商城相关API
│ ├── system/ # 系统相关API
│ ├── passport/ # 用户认证相关API
│ └── index.ts # API统一导出
├── assets/ # 静态资源
│ └── tabbar/ # 底部导航图标
├── components/ # 公共组件
│ ├── Header.tsx # 页面头部组件
│ ├── TabBar.tsx # 底部导航组件
│ ├── GoodsList.tsx # 商品列表组件
│ └── ...
├── hooks/ # 自定义Hooks
│ └── useCart.ts # 购物车Hook
├── pages/ # 页面文件
│ ├── index/ # 首页
│ ├── cart/ # 购物车页面
│ ├── order/ # 订单页面
│ └── user/ # 用户中心页面
├── passport/ # 用户认证页面
│ ├── login.tsx # 登录页面
│ ├── register.tsx # 注册页面
│ └── ...
├── cms/ # 内容管理页面
│ ├── category/ # 分类页面
│ └── detail/ # 详情页面
├── shop/ # 商城页面
│ ├── category/ # 商品分类
│ ├── goodsDetail/ # 商品详情
│ ├── orderConfirm/ # 订单确认
│ └── orderDetail/ # 订单详情
├── user/ # 用户相关页面
│ ├── profile/ # 个人资料
│ ├── address/ # 地址管理
│ ├── setting/ # 设置
│ └── ...
└── utils/ # 工具函数
├── request.ts # 网络请求封装
├── common.ts # 通用工具函数
├── time.ts # 时间处理工具
└── server.ts # 服务端相关工具
config/ # 配置文件目录
├── index.ts # Taro主配置
├── dev.ts # 开发环境配置
├── prod.ts # 生产环境配置
├── env.ts # 环境变量配置
└── app.ts # 应用配置
types/ # 类型定义
└── global.d.ts # 全局类型定义
```
## 🔧 开发规范
### 代码结构
- **Pages**:页面组件,负责页面逻辑和UI展示
- **Components**:公共组件,可复用的UI组件
- **Hooks**:自定义Hooks,封装业务逻辑
- **Utils**:工具函数,通用的功能函数
- **API**:接口定义,与后端API的交互
### 命名规范
- **组件名**:使用大驼峰命名法(PascalCase)
- **文件名**:使用小驼峰命名法(camelCase)或短横线命名法(kebab-case)
- **变量名**:使用小驼峰命名法(camelCase)
- **常量**:使用全大写,下划线分隔
- **CSS类名**:使用短横线命名法(kebab-case)
### TypeScript规范
- 优先使用接口(interface)定义类型
- 为函数参数和返回值添加类型注解
- 使用泛型提高代码复用性
- 避免使用 `any` 类型
## 📚 构建与部署
### 构建命令
```bash
# 构建微信小程序
# 微信小程序
npm run build:weapp
# 构建H5应用
# H5
npm run build:h5
# 构建支付宝小程序
npm run build:alipay
# 构建其他平台
npm run build:swan # 百度智能小程序
npm run build:tt # 字节跳动小程序
npm run build:qq # QQ小程序
npm run build:jd # 京东小程序
# 其他平台类似
npm run build:[platform]
```
### 主要API模块
项目API按功能模块组织:
## 📁 项目结构
- **用户认证**: `src/api/passport/` - 登录、注册、权限验证
- **用户管理**: `src/api/system/user/` - 用户信息管理
- **内容管理**: `src/api/cms/` - 文章、媒体文件管理
- **商城管理**: `src/api/shop/` - 商品、订单管理
- **系统管理**: `src/api/system/` - 系统配置、参数管理
```
shop-taro/
├── config/ # Taro 配置文件
│ ├── index.ts # 主配置
│ ├── dev.ts # 开发配置
│ └── prod.ts # 生产配置
├── src/ # 源代码
│ ├── app.config.ts # 应用配置(页面路由、tabBar等)
│ ├── app.tsx # 应用入口
│ ├── app.scss # 全局样式
│ ├── pages/ # 页面目录
│ │ ├── index/ # 首页
│ │ │ ├── index.tsx
│ │ │ ├── index.scss
│ │ │ └── index.config.ts
│ │ └── home/ # 个人中心页
│ │ ├── home.tsx
│ │ ├── home.scss
│ │ └── home.config.ts
│ ├── components/ # 自定义组件
│ ├── contexts/ # Context API
│ │ └── AppContext.tsx # 应用上下文
│ ├── hooks/ # 自定义 Hooks
│ │ └── useAppContext.ts
│ ├── utils/ # 工具函数
│ │ └── index.ts # 常用工具(日期、加密、防抖节流等)
│ ├── types/ # TypeScript 类型定义
│ │ └── global.d.ts # 全局类型声明
│ ├── styles/ # 样式文件
│ └── assets/ # 静态资源
│ └── tabbar/ # tabBar 图标
├── package.json # 项目依赖
├── tsconfig.json # TypeScript 配置
├── tailwind.config.js # TailwindCSS 配置
├── postcss.config.js # PostCSS 配置
├── project.config.json # 小程序项目配置
└── README.md # 项目说明
```
## 🚀 部署指南
## ✨ 特性
### 微信小程序部署
1. 构建小程序代码:
```bash
npm run build:weapp
```
2. 使用微信开发者工具打开 `dist` 目录
3. 点击"上传"按钮,上传代码到微信后台
4. 在微信公众平台提交审核
### 1. 主题切换
- 支持浅色/深色模式切换
- 使用 Context API 管理主题状态
- 暗黑模式适配
### H5应用部署
1. 构建H5应用:
```bash
npm run build:h5
```
2. 将 `dist` 目录下的文件上传到Web服务器
3. 配置Nginx或Apache服务器
4. 确保HTTPS访问(微信要求)
### 2. NutUI 组件库
- 已集成 @nutui/nutui-react-taro@2.7.4
- 支持按需引入
- 中文国际化配置
### 其他平台部署
各平台部署流程类似,构建对应平台代码后,使用相应的开发者工具上传即可。
### 3. TailwindCSS 集成
- 支持原子化 CSS 编写
- 暗黑模式支持(dark mode)
- 自定义主题色配置
### 生产环境配置
在生产环境中,需要修改相关配置:
### 4. 工具函数封装
- **日期处理**: 基于 Day.js 的格式化
- **加密解密**: MD5、AES 加密
- **性能优化**: 防抖、节流函数
- **数据操作**: 深拷贝等工具
1. **环境变量配置**:修改 `config/env.ts` 中的生产环境配置
```typescript
production: {
API_BASE_URL: 'https://your-production-api.com/api',
APP_NAME: 'Your App Name',
DEBUG: 'false',
}
```
### 5. TypeScript 支持
- 完整的类型定义
- 路径别名配置 `@/*`
- 严格模式开启
2. **构建配置**:修改 `config/prod.ts` 中的构建配置
- 启用代码压缩和混淆
- 配置CDN加速
- 优化资源加载
## 🎨 代码示例
3. **部署命令**:
```bash
NODE_ENV=production npm run build:weapp
```
### 使用 NutUI 组件
```tsx
import { Button, Cell, CellGroup } from '@nutui/nutui-react-taro'
export default function MyPage() {
return (
<CellGroup>
<Cell title="标题" description="描述" />
<Button type="primary" block>
提交
</Button>
</CellGroup>
)
}
```
### 使用 TailwindCSS
```tsx
<View className="p-4 bg-white dark:bg-gray-800">
<Text className="text-lg font-bold text-gray-800 dark:text-white">
暗黑模式适配文本
</Text>
</View>
```
### 使用 Context 管理状态
```tsx
import { useAppContext } from '@/hooks/useAppContext'
export default function MyPage() {
const { theme, toggleTheme } = useAppContext()
return (
<Button onClick={toggleTheme}>
当前主题: {theme}
</Button>
)
}
```
### 使用工具函数
```tsx
import { formatDate, md5, debounce } from '@/utils'
// 日期格式化
const dateStr = formatDate(new Date())
// MD5 加密
const encrypted = md5('hello world')
// 防抖
const handleSearch = debounce((keyword) => {
console.log(keyword)
}, 500)
```
## 📝 开发规范
### 文件命名
- 页面目录: `kebab-case` (如 `user-profile/`)
- 组件文件: `PascalCase` (如 `UserProfile.tsx`)
- 工具文件: `camelCase` (如 `formatDate.ts`)
### 代码风格
- 使用 ESLint + @typescript-eslint 进行代码检查
- 使用 2 空格缩进
- 分号结尾
- 单引号优先
### 提交规范
```
feat: 新功能
fix: 修复 bug
docs: 文档更新
style: 代码格式调整
refactor: 重构
test: 测试相关
chore: 构建/工具配置
```
## 🔧 常见问题
### 开发环境问题
1. **Node.js版本问题**:确保使用Node.js 16+版本
2. **依赖安装失败**:尝试清除缓存后重新安装
```bash
npm cache clean --force
npm install
```
3. **编译错误**:检查TypeScript配置和代码语法
### 1. TailwindCSS 样式不生效?
检查 `postcss.config.js` 配置,确保已安装 `postcss-nested`。
### 小程序开发问题
1. **微信开发者工具无法预览**:检查appid配置和网络连接
2. **API请求失败**:确认后端服务正常运行,检查域名白名单
3. **样式显示异常**:检查rpx单位使用和NutUI组件样式
### 2. NutUI 组件样式丢失?
确保在 `app.scss` 中引入了 NutUI 样式:
```scss
@import '@nutui/nutui-react-taro/dist/styles/vitamin.css';
```
### 性能优化建议
1. **代码分割**:使用Taro的分包功能减少主包大小
2. **图片优化**:使用WebP格式,启用图片懒加载
3. **请求优化**:合并API请求,使用缓存机制
### 3. TypeScript 路径别名报错?
检查 `tsconfig.json` 的 `paths` 配置,并确保 IDE 正确识别。
## 🤝 贡献指南
## 📚 相关文档
1. Fork 本仓库
2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
4. 推送到分支 (`git push origin feature/AmazingFeature`)
5. 打开 Pull Request
- [Taro 官方文档](https://taro-docs.jd.com/)
- [React 官方文档](https://react.dev/)
- [NutUI React 文档](https://nutui.jd.com/react-taro/)
- [TailwindCSS 文档](https://tailwindcss.com/docs)
- [TypeScript 文档](https://www.typescriptlang.org/docs/)
## 📄 许可证
## 📄 License
本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情
## 📞 联系我们
- 官网:https://websoft.top
- 邮箱:170083662@qq.com
- QQ群:479713884
MIT
---
⭐ 如果这个项目对您有帮助,请给我们一个星标!
**构建时间**: 2026-05-10
**作者**: Senior Developer
-209
View File
@@ -1,209 +0,0 @@
# 微信小程序扫码登录功能实现
## 项目概述
本项目已完整实现微信小程序扫码登录功能,支持用户通过小程序扫描网页端二维码快速登录。
## 🎯 功能特性
- ✅ **完整的后端API** - Java Spring Boot实现
- ✅ **多种前端集成方式** - 按钮、弹窗、页面
- ✅ **智能二维码解析** - 支持URL、JSON、纯token格式
- ✅ **安全可靠** - Token有效期控制,防重复使用
- ✅ **用户体验优秀** - 实时状态反馈,错误处理完善
- ✅ **微信深度集成** - 自动获取用户信息
## 📁 项目结构
### 后端 (Java)
```
auto/
├── controller/QrLoginController.java # REST API控制器
├── service/QrLoginService.java # 业务接口
├── service/impl/QrLoginServiceImpl.java # 业务实现
└── dto/ # 数据传输对象
├── QrLoginData.java
├── QrLoginConfirmRequest.java
├── QrLoginStatusResponse.java
└── QrLoginGenerateResponse.java
```
### 前端 (小程序)
```
src/
├── api/qr-login/index.ts # API接口层
├── hooks/useQRLogin.ts # 业务逻辑Hook
├── components/ # 组件层
│ ├── QRLoginButton.tsx # 扫码按钮组件
│ ├── QRLoginScanner.tsx # 扫码器组件
│ ├── QRScanModal.tsx # 扫码弹窗组件
│ └── QRLoginDemo.tsx # 演示组件
└── pages/ # 页面层
├── qr-login/index.tsx # 扫码登录页面
├── qr-confirm/index.tsx # 登录确认页面
└── qr-test/index.tsx # 功能测试页面
```
## 🚀 快速开始
### 1. 后端配置
确保Java后端服务正常运行,API接口可访问:
- `POST /api/qr-login/generate` - 生成扫码token
- `GET /api/qr-login/status/{token}` - 检查登录状态
- `POST /api/qr-login/confirm` - 确认登录
- `POST /api/qr-login/scan/{token}` - 扫码操作
### 2. 前端使用
#### 最简单的使用方式:
```tsx
import QRLoginButton from '@/components/QRLoginButton';
<QRLoginButton />
```
#### 弹窗方式:
```tsx
import QRScanModal from '@/components/QRScanModal';
<QRScanModal
visible={showModal}
onClose={() => setShowModal(false)}
onSuccess={(result) => console.log('登录成功', result)}
/>
```
#### 页面跳转方式:
```tsx
import Taro from '@tarojs/taro';
Taro.navigateTo({
url: '/passport/qr-login/index'
});
```
## 🔧 支持的二维码格式
系统智能识别多种二维码格式:
1. **URL格式**:`https://mp.websoft.top/qr-confirm?qrCodeKey=token123`
2. **JSON格式**:`{"token": "token123", "type": "qr-login"}`
3. **简单格式**:`qr-login:token123` 或直接 `token123`
## 📱 页面说明
### 1. 扫码登录页面 (`/passport/qr-login/index`)
- 完整的扫码登录功能
- 用户信息显示
- 登录历史记录
- 使用说明和安全提示
### 2. 登录确认页面 (`/passport/qr-confirm/index`)
- 处理二维码跳转确认
- 支持URL参数:`qrCodeKey` 或 `token`
- 用户确认界面
### 3. 功能测试页面 (`/passport/qr-test/index`)
- 演示各种集成方式
- 功能测试和调试
## 🛠️ 开发指南
### 1. 添加扫码按钮到现有页面
```tsx
import QRLoginButton from '@/components/QRLoginButton';
const MyPage = () => {
return (
<View>
<QRLoginButton
text="扫码登录"
onSuccess={(result) => {
// 处理登录成功
console.log('用户登录成功:', result);
}}
onError={(error) => {
// 处理登录失败
console.error('登录失败:', error);
}}
/>
</View>
);
};
```
### 2. 自定义扫码逻辑
```tsx
import { useQRLogin } from '@/hooks/useQRLogin';
const MyComponent = () => {
const {
startScan,
isLoading,
isSuccess,
result,
error
} = useQRLogin();
return (
<Button
loading={isLoading}
onClick={startScan}
>
{isLoading ? '扫码中...' : '扫码登录'}
</Button>
);
};
```
## 🔒 安全注意事项
1. **用户登录验证**:使用前确保用户已在小程序中登录
2. **Token有效期**:二维码5分钟有效期,过期自动失效
3. **权限申请**:确保小程序已申请摄像头权限
4. **来源验证**:只扫描来自官方网站的登录二维码
## 🐛 常见问题
### Q: 提示"请先登录小程序"
A: 用户需要先在小程序中完成登录,获取用户ID和访问令牌。
### Q: 提示"无效的登录二维码"
A: 检查二维码格式是否正确,或者二维码是否已过期。
### Q: 扫码失败
A: 检查摄像头权限,确保二维码清晰可见。
### Q: 网络请求失败
A: 检查网络连接和API接口地址配置。
## 📚 相关文档
- [详细使用指南](docs/QR_LOGIN_USAGE.md)
- [API接口文档](src/api/qr-login/index.ts)
- [组件API文档](docs/QR_LOGIN_USAGE.md#组件api)
## 🎉 测试功能
访问测试页面验证功能:
```
/pages/qr-test/index
```
该页面包含所有集成方式的演示和测试功能。
## 📞 技术支持
如有问题,请检查:
1. 后端API服务是否正常运行
2. 小程序用户是否已登录
3. 网络连接是否正常
4. 二维码格式是否正确
---
**开发者**: 科技小王子
**更新时间**: 2025-09-20
+7 -20
View File
@@ -1,24 +1,11 @@
// babel-preset-taro 更多选项和默认值:
// https://github.com/NervJS/taro/blob/next/packages/babel-preset-taro/README.md
module.exports = {
presets: [
['taro',
{
framework: 'react',
ts: 'true',
compiler: 'webpack5',
}]
"presets": [
["taro", {
"framework": "react",
"ts": true
}]
],
plugins: [
[
"import",
{
"libraryName": "@nutui/nutui-react-taro",
"libraryDirectory": "dist/esm",
"style": 'css',
"camel2DashComponentName": false
},
'nutui-react-taro'
]
"plugins": [
"@babel/plugin-transform-runtime"
]
}
+7 -13
View File
@@ -1,14 +1,8 @@
import { API_BASE_URL } from './env'
import { API_BASE_URL, SERVER_API_URL, WEBSOPY_API_BASE_URL } from './env'
// 租户ID - 请根据实际情况修改
export const TenantId = '5';
// 租户名称
export const TenantName = '网宿软件';
// 接口地址 - 请根据实际情况修改
export const BaseUrl = API_BASE_URL;
// 当前版本
export const Version = 'v3.0.8';
// 版权信息
export const Copyright = 'WebSoft Inc.';
// java -jar CertificateDownloader.jar -k 0kF5OlPr482EZwtn9zGufUcqa7ovgxRL -m 1723321338 -f ./apiclient_key.pem -s 2B933F7C35014A1C363642623E4A62364B34C4EB -o ./
export const TenantId = '5'
export const TenantName = '威硕开发助手'
export const BaseUrl = API_BASE_URL
export const ServerBaseUrl = SERVER_API_URL
export const WebsopyBaseUrl = WEBSOPY_API_BASE_URL
export const Version = 'v3.0.0'
+14 -10
View File
@@ -1,13 +1,17 @@
import type { UserConfigExport } from "@tarojs/cli";
export default {
logger: {
quiet: false,
stats: true
env: {
NODE_ENV: '"development"'
},
mini: {
miniCssExtractPluginOption: {
ignoreOrder: true
defineConstants: {
__DEV__: JSON.stringify(true)
},
mini: {},
h5: {
devServer: {
host: 'localhost',
port: 10086,
hot: true,
open: false
}
},
h5: {}
} satisfies UserConfigExport<'webpack5'>
}
}
+30
View File
@@ -0,0 +1,30 @@
export const CURRENT_ENV: string
export const ENV_CONFIG: {
development: {
API_BASE_URL: string
SERVER_API_URL: string
WEBSOPY_API_BASE_URL: string
APP_NAME: string
DEBUG: string
}
test: {
API_BASE_URL: string
SERVER_API_URL: string
WEBSOPY_API_BASE_URL: string
APP_NAME: string
DEBUG: string
}
production: {
API_BASE_URL: string
SERVER_API_URL: string
WEBSOPY_API_BASE_URL: string
APP_NAME: string
DEBUG: string
}
}
export function getEnvConfig(): (typeof ENV_CONFIG)[keyof typeof ENV_CONFIG]
export const API_BASE_URL: string
export const SERVER_API_URL: string
export const WEBSOPY_API_BASE_URL: string
export const APP_NAME: string
export const DEBUG: string
+31
View File
@@ -0,0 +1,31 @@
const CURRENT_ENV = process.env.NODE_ENV === 'production' ? 'production' : 'development'
export const ENV_CONFIG = {
development: {
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: '威硕谱软件',
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: '威硕谱软件',
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: '威硕谱软件',
DEBUG: 'false',
},
}
export function getEnvConfig() {
return ENV_CONFIG[CURRENT_ENV] || ENV_CONFIG.development
}
export const { API_BASE_URL, SERVER_API_URL, WEBSOPY_API_BASE_URL, APP_NAME, DEBUG } = getEnvConfig()
-43
View File
@@ -1,43 +0,0 @@
// 环境变量配置
export const ENV_CONFIG = {
// 开发环境
development: {
// API_BASE_URL: 'http://127.0.0.1:9200/api',
API_BASE_URL: 'https://websopy-api.websoft.top/api',
APP_NAME: '开发环境',
DEBUG: 'true',
},
// 生产环境
production: {
API_BASE_URL: 'https://websopy-api.websoft.top/api',
APP_NAME: '网宿软件',
DEBUG: 'false',
},
// 测试环境
test: {
API_BASE_URL: 'https://websopy-api.websoft.top/api',
APP_NAME: '测试环境',
DEBUG: 'true',
}
}
// 获取当前环境配置
export function getEnvConfig() {
const env = process.env.NODE_ENV || 'development'
if (env === 'production') {
return ENV_CONFIG.production
} else { // @ts-ignore
if (env === 'test') {
return ENV_CONFIG.test
} else {
return ENV_CONFIG.development
}
}
}
// 导出环境变量
export const {
API_BASE_URL,
APP_NAME,
DEBUG
} = getEnvConfig()
+55 -53
View File
@@ -1,97 +1,108 @@
import { defineConfig, type UserConfigExport } from '@tarojs/cli'
import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin'
import { UnifiedWebpackPluginV5 } from 'weapp-tailwindcss/webpack'
import devConfig from './dev'
import prodConfig from './prod'
import { getEnvConfig } from './env'
// import vitePluginImp from 'vite-plugin-imp'
// https://taro-docs.jd.com/docs/next/config#defineconfig-辅助函数
export default defineConfig<'webpack5'>(async (merge, {}) => {
import path from 'path'
import { getEnvConfig } from './env.js'
// https://taro-docs.jd.com/docs/next/config
export default defineConfig<'webpack5'>(async (merge, { mode }) => {
const baseConfig: UserConfigExport<'webpack5'> = {
projectName: 'websoft-react',
date: '2024-12-30',
plugins: ['@tarojs/plugin-html'],
designWidth (input:any) {
// 配置 NutUI 375 尺寸
if (input?.file?.replace(/\\+/g, '/').indexOf('@nutui') > -1) {
return 375
}
// 全局使用 Taro 默认的 750 尺寸
return 750
},
// 项目配置
projectName: 'shop-taro',
date: '2026-5-10',
designWidth: 750,
deviceRatio: {
640: 2.34 / 2,
750: 1,
828: 1.81 / 2,
375: 2 / 1
828: 1.81 / 2
},
sourceRoot: 'src',
outputRoot: 'dist',
plugins: [
'@tarojs/plugin-platform-weapp',
'@tarojs/plugin-platform-h5'
],
// 编译配置
defineConstants: {
API_BASE_URL: JSON.stringify(getEnvConfig().API_BASE_URL),
SERVER_API_URL: JSON.stringify(getEnvConfig().SERVER_API_URL),
APP_NAME: JSON.stringify(getEnvConfig().APP_NAME),
DEBUG: JSON.stringify(getEnvConfig().DEBUG)
DEBUG: JSON.stringify(getEnvConfig().DEBUG),
},
copy: {
patterns: [
],
options: {
}
patterns: [],
options: {}
},
framework: 'react',
compiler: {
type: 'webpack5',
prebundle: {
exclude: ['@nutui/nutui-react-taro', '@nutui/icons-react-taro'],
enable: false
}
},
// 开启持久化缓存 - 有效提升二次编译速度
cache: {
enable: false // Webpack 持久化缓存配置,建议开启。默认配置请参考:https://docs.taro.zone/docs/config-detail#cache
enable: false,
type: 'filesystem',
cacheDirectory: path.resolve(__dirname, '../.webpack-cache'),
buildDependencies: {
config: [__filename]
}
},
// 小程序配置
mini: {
postcss: {
pxtransform: {
enable: true,
config: {}
},
url: {
enable: true,
config: {
selectorBlackList: ['nut-']
limit: 1024
}
},
cssModules: {
enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
enable: false,
config: {
namingPattern: 'module', // 转换模式,取值为 global/module
namingPattern: 'module',
generateScopedName: '[name]__[local]___[hash:base64:5]'
}
}
},
webpackChain(chain) {
chain.resolve.plugin('tsconfig-paths').use(TsconfigPathsPlugin)
// weapp-tailwindcss: 自动处理 TailwindCSS 与 WXSS 的兼容性
chain.merge({
plugin: {
'weapp-tailwindcss': {
plugin: UnifiedWebpackPluginV5,
args: [{
appType: 'taro',
rem2rpx: true,
}]
}
}
})
}
},
// H5 配置
h5: {
publicPath: '/',
staticDirectory: 'static',
output: {
filename: 'js/[name].[hash:8].js',
chunkFilename: 'js/[name].[chunkhash:8].js'
},
miniCssExtractPluginOption: {
ignoreOrder: true,
filename: 'css/[name].[hash].css',
chunkFilename: 'css/[name].[chunkhash].css'
},
postcss: {
autoprefixer: {
enable: true,
config: {}
enable: true
},
cssModules: {
enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
enable: false,
config: {
namingPattern: 'module', // 转换模式,取值为 global/module
namingPattern: 'module',
generateScopedName: '[name]__[local]___[hash:base64:5]'
}
}
@@ -100,19 +111,10 @@ export default defineConfig<'webpack5'>(async (merge, {}) => {
chain.resolve.plugin('tsconfig-paths').use(TsconfigPathsPlugin)
}
},
rn: {
appName: 'taroDemo',
postcss: {
cssModules: {
enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
}
}
}
}
if (process.env.NODE_ENV === 'development') {
// 本地开发构建配置(不混淆压缩)
if (mode === 'development') {
return merge({}, baseConfig, devConfig)
}
// 生产构建配置(默认开启压缩混淆等)
return merge({}, baseConfig, prodConfig)
})
+10 -34
View File
@@ -1,36 +1,12 @@
import type { UserConfigExport } from "@tarojs/cli";
export default {
mini: {
miniCssExtractPluginOption: {
ignoreOrder: true
}
env: {
NODE_ENV: '"production"'
},
h5: {
/**
* WebpackChain 插件配置
* @docs https://github.com/neutrinojs/webpack-chain
*/
// webpackChain (chain) {
// /**
// * 如果 h5 端编译后体积过大,可以使用 webpack-bundle-analyzer 插件对打包体积进行分析。
// * @docs https://github.com/webpack-contrib/webpack-bundle-analyzer
// */
// chain.plugin('analyzer')
// .use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin, [])
// /**
// * 如果 h5 端首屏加载时间过长,可以使用 prerender-spa-plugin 插件预加载首页。
// * @docs https://github.com/chrisvfritz/prerender-spa-plugin
// */
// const path = require('path')
// const Prerender = require('prerender-spa-plugin')
// const staticDir = path.join(__dirname, '..', 'dist')
// chain
// .plugin('prerender')
// .use(new Prerender({
// staticDir,
// routes: [ '/pages/index/index' ],
// postProcess: (context) => ({ ...context, outputPath: path.join(staticDir, 'index.html') })
// }))
// }
}
} satisfies UserConfigExport<'webpack5'>
defineConstants: {
__DEV__: JSON.stringify(false)
},
mini: {
compressTemplate: true
},
h5: {}
}
-54
View File
@@ -1,54 +0,0 @@
export type ProductMatrixItem = {
title: string
recommend?: boolean
desc: string
tags: string[]
adminUrl: string
}
// Keep in sync with PC (nuxt4-10398) homepage "产品矩阵".
export const productMatrix: ProductMatrixItem[] = [
{
title: '企业官网',
recommend: true,
desc: '品牌展示与获客转化,支持多模板、多语言、SEO 与可视化配置。',
tags: ['模板', 'SEO', '多语言', '私有化'],
adminUrl: 'https://site.websoft.top'
},
{
title: '小程序/公众号',
recommend: false,
desc: '多端渠道接入与统一管理,适配常见内容与电商场景。',
tags: ['多端', '渠道', '可扩展'],
adminUrl: 'https://mp.websoft.top'
},
{
title: '电商系统',
recommend: true,
desc: '商品/订单/支付/营销基础能力,插件化扩展,支持多端触达。',
tags: ['支付', '插件', '营销', '多租户'],
adminUrl: 'https://shop.websoft.top'
},
{
title: '管理后台',
recommend: false,
desc: '多租户管理、角色权限、组织架构与可扩展菜单体系。',
tags: ['权限', '多租户', '审计'],
adminUrl: 'https://oa.websoft.top'
},
{
title: '开发者中心',
recommend: false,
desc: '应用开发与交付入口:应用中心、源码仓库、Git 账号绑定、权限申请与教程文档。',
tags: ['应用', '源码', 'Git', '教程'],
adminUrl: '/developer'
},
{
title: '模板/插件市场',
recommend: false,
desc: '支持模板与插件购买、授权与更新,形成生态与增值体系。',
tags: ['市场', '授权', '更新', '变现'],
adminUrl: '/market'
}
]
-445
View File
@@ -1,445 +0,0 @@
# 小程序改造方案 - 开发者中心 & 企业控制台
> 制定时间:2026-04-12
> 项目中心:以「项目」为核心构建所有功能
---
## 一、整体架构设计
### 1.1 用户角色划分
```
┌─────────────────────────────────────────────────────────────┐
│ 小程序端 │
├─────────────────────────┬───────────────────────────────────┤
│ 🛠️ 开发者角色 │ 🏢 企业客户角色 │
│ (Developer) │ (Enterprise) │
├─────────────────────────┼───────────────────────────────────┤
│ • 拥有开发者权限的用户 │ • 拥有企业权限的用户 │
│ • 可开发/发布应用 │ • 可管理企业应用、成员、账单 │
│ • 独立于企业存在 │ • 属于某个企业组织 │
│ │ • 可申请成为开发者 │
└─────────────────────────┴───────────────────────────────────┘
```
### 1.2 角色获取方式
| 角色 | 获取方式 | 说明 |
|------|---------|------|
| 开发者 | 申请审核制 | 用户主动申请 → 资料审核 → 获得开发者权限 |
| 企业客户 | 购买产品 | 下单支付 → 自动开通企业账号 → 成为企业主 |
---
## 二、页面结构设计
### 2.1 入口设计
```
首页 (pages/index/index)
├── 产品展示区
├── 快捷入口区
│ ├── 🛠️ 开发者中心 (开发者角色入口)
│ └── 🏢 企业控制台 (企业客户入口)
└── 底部 TabBar (商城/购物车/我的)
```
**注意**:两个入口对普通用户可见,但需要对应角色权限才能进入。
### 2.2 开发者中心 (developer/)
```
developer/
├── index.tsx # 开发者工作台
│ ├── 我的项目列表
│ ├── 快捷操作 (新建项目、查看文档)
│ └── 开发者动态/通知
│
├── project/
│ ├── index.tsx # 项目列表
│ ├── create.tsx # 创建项目
│ └── [id]/
│ ├── index.tsx # 项目概览
│ ├── settings.tsx # 项目设置
│ ├── members.tsx # 项目成员
│ ├── api-keys.tsx # API Key 管理
│ ├── deployments.tsx # 部署记录
│ └── logs.tsx # 运行日志
│
├── app/ # 应用管理
│ ├── index.tsx # 应用列表
│ ├── create.tsx # 创建应用
│ └── [id]/
│ ├── index.tsx # 应用详情
│ ├── version.tsx # 版本管理
│ ├── config.tsx # 应用配置
│ └── publish.tsx # 发布管理
│
├── market/ # 开发者市场
│ ├── index.tsx # 市场首页
│ ├── templates.tsx # 模板市场
│ └── plugins.tsx # 插件市场
│
├── orders/ # 开发订单 (已有)
│ └── index.tsx
│
├── developer/ # 开发者中心
│ ├── index.tsx # 开发者设置
│ ├── profile.tsx # 个人资料
│ ├── credentials.tsx # 开发者凭证
│ └── notification.tsx # 通知设置
│
└── docs/ # 文档中心
├── index.tsx # 文档首页
├── quickstart.tsx # 快速开始
├── api-docs.tsx # API 文档
└── sdk-download.tsx # SDK 下载
```
### 2.3 企业控制台 (enterprise/)
```
enterprise/
├── index.tsx # 企业工作台
│ ├── 企业概览仪表盘
│ ├── 快捷入口 (应用/成员/账单)
│ └── 最新动态
│
├── apps/ # 企业应用
│ ├── index.tsx # 应用列表
│ ├── [id]/
│ │ ├── index.tsx # 应用详情
│ │ ├── monitor.tsx # 运营监控
│ │ ├── analytics.tsx # 数据分析
│ │ └── settings.tsx # 应用设置
│ └── purchase.tsx # 购买应用
│
├── members/ # 成员管理
│ ├── index.tsx # 成员列表
│ ├── invite.tsx # 邀请成员
│ ├── roles.tsx # 角色权限
│ └── audit.tsx # 权限审批
│
├── orders/ # 订单账单
│ ├── index.tsx # 订单列表
│ ├── detail.tsx # 订单详情
│ ├── invoice.tsx # 发票管理
│ └── bills.tsx # 账单明细
│
├── billing/ # 费用中心
│ ├── index.tsx # 费用概览
│ ├── consumption.tsx # 消费明细
│ ├── recharge.tsx # 充值中心
│ └── coupons.tsx # 优惠券
│
├── enterprise/ # 企业设置
│ ├── index.tsx # 企业设置
│ ├── info.tsx # 企业信息
│ ├── domain.tsx # 域名配置
│ ├── security.tsx # 安全设置
│ └── notification.tsx # 通知设置
│
└── developer/ # 开发者入口
├── index.tsx # 开发者介绍
└── apply.tsx # 申请开发者
```
---
## 三、核心功能设计
### 3.1 项目管理 (项目中心)
**项目类型**:
| 类型 | 说明 | 适用角色 |
|------|------|---------|
| 基础项目 | 免费,限制功能 | 所有开发者 |
| 专业项目 | 付费,功能完整 | 开发者/企业 |
| 企业项目 | 全功能,支持团队协作 | 企业 |
**项目核心属性**:
```typescript
interface Project {
id: string
name: string // 项目名称
type: 'basic' | 'pro' | 'enterprise'
owner: string // 所有者 (开发者ID 或 企业ID)
ownerType: 'developer' | 'enterprise'
description?: string // 项目描述
icon?: string // 项目图标
status: 'active' | 'suspended' | 'archived'
createdAt: string
updatedAt: string
// 关联信息
apps: string[] // 关联的应用
members: ProjectMember[] // 项目成员
apiKeys: ApiKey[] // API Key
// 统计
appCount: number
memberCount: number
apiCallCount: number // API 调用次数
storageUsed: number // 存储使用量
}
```
### 3.2 开发者角色功能
**权限项**:
| 权限 | 说明 | 默认 |
|------|------|------|
| project.create | 创建项目 | ✅ |
| project.edit | 编辑项目 | ✅ |
| project.delete | 删除项目 | ✅ |
| app.create | 创建应用 | ✅ |
| app.publish | 发布应用 | ✅ |
| apiKey.manage | 管理 API Key | ✅ |
| market.access | 访问市场 | ✅ |
| docs.access | 访问文档 | ✅ |
**开发者特有功能**:
- ✅ 创建和管理个人项目
- ✅ 开发、测试、发布应用
- ✅ 访问模板/插件市场
- ✅ 生成和管理 API Key
- ✅ 查看开发者文档和 SDK
- ✅ 提交工单和技术支持
### 3.3 企业客户角色功能
**权限项**:
| 权限 | 说明 | 适用角色 |
|------|------|------|
| enterprise.manage | 企业管理 | 企业主 |
| app.manage | 应用管理 | 企业主/管理员 |
| member.invite | 邀请成员 | 企业主/管理员 |
| member.role | 分配角色 | 企业主/管理员 |
| order.view | 查看订单 | 所有成员 |
| billing.view | 查看账单 | 企业主/财务 |
| billing.pay | 支付充值 | 企业主/财务 |
**企业特有功能**:
- ✅ 企业仪表盘和数据概览
- ✅ 应用运营监控
- ✅ 成员邀请和权限管理
- ✅ 订单管理和发票
- ✅ 费用中心和充值
- ✅ 企业信息配置
### 3.4 角色切换机制
```
用户登录
│
├── 开发者身份
│ └── 可进入开发者中心
│
├── 企业身份
│ └── 可进入企业控制台
│
└── 两者皆有
└── 可切换身份入口
├── [开发者] 🛠️ 开发者中心
└── [企业] 🏢 企业控制台
```
---
## 四、开发优先级与里程碑
### Phase 1:基础框架 ⭐ 必须上线
**目标**:建立核心框架,实现最小可用产品
| 模块 | 页面 | 工作量 | 优先级 |
|------|------|--------|--------|
| 入口设计 | 首页快捷入口 + 身份切换 | ⭐⭐ | P0 |
| 开发者工作台 | 开发者首页/概览 | ⭐⭐⭐ | P0 |
| 项目管理 | 项目列表 + 创建 | ⭐⭐⭐⭐ | P0 |
| 应用管理 | 应用列表 + 创建 | ⭐⭐⭐⭐ | P0 |
| 企业工作台 | 企业首页/概览 | ⭐⭐⭐ | P0 |
| 企业应用 | 应用列表 + 详情 | ⭐⭐⭐⭐ | P0 |
### Phase 2:核心功能 ⭐ 必须上线
**目标**:完成核心业务闭环
| 模块 | 页面 | 工作量 | 优先级 |
|------|------|--------|--------|
| API Key | 生成/查看/禁用 | ⭐⭐⭐ | P0 |
| 项目成员 | 邀请/移除/角色 | ⭐⭐⭐ | P0 |
| 应用发布 | 版本管理/发布 | ⭐⭐⭐ | P0 |
| 企业成员 | 列表/邀请/角色 | ⭐⭐⭐⭐ | P0 |
| 订单管理 | 列表/详情 | ⭐⭐⭐ | P0 |
| 账单查看 | 消费明细 | ⭐⭐⭐ | P0 |
### Phase 3:增强功能 🔄 后续迭代
| 模块 | 页面 | 工作量 | 优先级 |
|------|------|--------|--------|
| 开发者申请 | 申请流程/审核 | ⭐⭐⭐ | P1 |
| 权限审批 | 权限申请/审批 | ⭐⭐⭐ | P1 |
| 市场浏览 | 模板/插件市场 | ⭐⭐⭐⭐ | P1 |
| 运营监控 | 数据看板 | ⭐⭐⭐⭐ | P1 |
| 开发者文档 | API文档/SDK | ⭐⭐⭐⭐ | P1 |
| 消息通知 | 通知中心 | ⭐⭐⭐ | P1 |
### Phase 4:高级功能 🔄 后续迭代
| 模块 | 页面 | 工作量 | 优先级 |
|------|------|--------|--------|
| CI/CD | 流水线管理 | ⭐⭐⭐⭐⭐ | P2 |
| 部署管理 | 一键部署 | ⭐⭐⭐⭐ | P2 |
| 数据分析 | 高级统计 | ⭐⭐⭐⭐ | P2 |
| 工单系统 | 技术支持 | ⭐⭐⭐ | P2 |
| 发票管理 | 电子发票 | ⭐⭐⭐ | P2 |
| SSO | 单点登录 | ⭐⭐⭐⭐⭐ | P2 |
---
## 五、与现有模块的关系
### 5.1 独立开发,不依赖现有模块
| 新建模块 | 说明 |
|---------|------|
| developer/ | 开发者中心全套功能 |
| enterprise/ | 企业控制台全套功能 |
### 5.2 共享基础设施
| 共享内容 | 说明 |
|---------|------|
| 用户认证 | 复用 passport 模块的登录体系 |
| 用户状态 | 复用 globalData/storage 中的用户信息 |
| 消息通知 | 可复用 notification 组件 |
| 基础 UI | 复用组件库样式 |
### 5.3 不复用模块
- ❌ 不复用 shop/ (商城) - 独立的企业应用体系
- ❌ 不复用 dealer/ (分销) - 独立的开发者体系
- ❌ 不复用 user/order (用户订单) - 使用新的企业订单体系
---
## 六、技术实现建议
### 6.1 目录结构
```
src/
├── developer/ # 🛠️ 开发者模块
│ ├── index.tsx # 开发者入口
│ ├── project/ # 项目管理
│ ├── app/ # 应用管理
│ ├── market/ # 市场
│ └── docs/ # 文档
│
├── enterprise/ # 🏢 企业模块
│ ├── index.tsx # 企业入口
│ ├── apps/ # 企业应用
│ ├── members/ # 成员管理
│ ├── orders/ # 订单账单
│ ├── billing/ # 费用中心
│ └── settings/ # 企业设置
│
└── shared/ # 共享
├── components/ # 共享组件
├── types/ # 类型定义
└── api/ # API 接口
```
### 6.2 路由配置
```typescript
// app.config.ts
export default {
subpackages: [
{
root: "developer",
pages: [
"index",
"project/index",
"project/create",
"project/[id]/index",
"project/[id]/settings",
"app/index",
"app/create",
"app/[id]/index",
// ...
]
},
{
root: "enterprise",
pages: [
"index",
"apps/index",
"apps/[id]/index",
"members/index",
"members/invite",
"orders/index",
"orders/detail",
// ...
]
}
]
}
```
---
## 七、API 接口设计建议
### 7.1 项目相关
| 接口 | 方法 | 说明 |
|------|------|------|
| /api/project/list | GET | 项目列表 |
| /api/project/create | POST | 创建项目 |
| /api/project/:id | GET/PUT/DELETE | 项目 CRUD |
| /api/project/:id/members | GET/POST/DELETE | 项目成员 |
| /api/project/:id/keys | GET/POST/DELETE | API Key |
### 7.2 应用相关
| 接口 | 方法 | 说明 |
|------|------|------|
| /api/app/list | GET | 应用列表 |
| /api/app/create | POST | 创建应用 |
| /api/app/:id | GET/PUT/DELETE | 应用 CRUD |
| /api/app/:id/version | POST | 发布版本 |
### 7.3 企业相关
| 接口 | 方法 | 说明 |
|------|------|------|
| /api/enterprise/info | GET | 企业信息 |
| /api/enterprise/members | GET/POST | 成员管理 |
| /api/enterprise/roles | GET/POST | 角色管理 |
| /api/enterprise/orders | GET | 订单列表 |
| /api/enterprise/bills | GET | 账单明细 |
---
## 八、下一步行动
### 立即开始 (Phase 1)
1. ✅ 创建 developer/ 和 enterprise/ 目录结构
2. ✅ 设计 API 接口文档 (与后端对齐)
3. ✅ 开发首页入口组件
4. ✅ 开发工作台基础页面
### 需要确认
- [ ] 后端 API 是否已准备好?
- [ ] 数据库表结构是否已设计?
- [ ] 是否需要支持微信小程序码扫码登录?
---
**文档版本**:v1.0
**最后更新**:2026-04-12
-1
View File
@@ -1 +0,0 @@
import '@tarojs/rn-supporter/entry-file.js'
-14
View File
@@ -1,14 +0,0 @@
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config')
const { getMetroConfig } = require('@tarojs/rn-supporter')
/**
* Metro configuration
* https://facebook.github.io/metro/docs/configuration
*
* @type {import('metro-config').MetroConfig}
*/
const config = {}
module.exports = (async function () {
return mergeConfig(getDefaultConfig(__dirname), await getMetroConfig(), config)
})()
+316
View File
@@ -0,0 +1,316 @@
{
"models": [
{
"id": "auto",
"name": "AuTo",
"vendor": "Custom",
"url": "https://api.getaigate.com/v1/chat/completions",
"apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb",
"maxInputTokens": 128000,
"maxOutputTokens": 32000,
"supportsToolCall": true,
"supportsImages": true,
"supportsReasoning": true,
"useCustomProtocol": false
},
{
"id": "deepseek-v4-pro",
"name": "Deepseek-v4-pro",
"vendor": "Custom",
"url": "https://api.getaigate.com/v1/chat/completions",
"apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb",
"maxInputTokens": 1000000,
"maxOutputTokens": 32000,
"supportsToolCall": true,
"supportsImages": true,
"supportsReasoning": true,
"useCustomProtocol": false
},
{
"id": "deepseek-v4-flash",
"name": "Deepseek-v4-flash",
"vendor": "Custom",
"url": "https://api.getaigate.com/v1/chat/completions",
"apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb",
"maxInputTokens": 1000000,
"maxOutputTokens": 32000,
"supportsToolCall": true,
"supportsImages": true,
"supportsReasoning": true,
"useCustomProtocol": false
},
{
"id": "deepseek-v3-2-volc",
"name": "Deepseek-v3-2-volc",
"vendor": "Custom",
"url": "https://api.getaigate.com/v1/chat/completions",
"apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb",
"maxInputTokens": 128000,
"maxOutputTokens": 32000,
"supportsToolCall": true,
"supportsImages": true,
"supportsReasoning": true,
"useCustomProtocol": false
},
{
"id": "deepseek-v3-1",
"name": "Deepseek-v3-1",
"vendor": "Custom",
"url": "https://api.getaigate.com/v1/chat/completions",
"apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb",
"maxInputTokens": 128000,
"maxOutputTokens": 32000,
"supportsToolCall": true,
"supportsImages": true,
"supportsReasoning": true,
"useCustomProtocol": false
},
{
"id": "deepseek-v3-0324",
"name": "Deepseek-v3-0324",
"vendor": "Custom",
"url": "https://api.getaigate.com/v1/chat/completions",
"apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb",
"maxInputTokens": 128000,
"maxOutputTokens": 32000,
"supportsToolCall": true,
"supportsImages": true,
"supportsReasoning": true,
"useCustomProtocol": false
},
{
"id": "deepseek-r1",
"name": "Deepseek-r1",
"vendor": "Custom",
"url": "https://api.getaigate.com/v1/chat/completions",
"apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb",
"maxInputTokens": 128000,
"maxOutputTokens": 32000,
"supportsToolCall": true,
"supportsImages": true,
"supportsReasoning": true,
"useCustomProtocol": false
},
{
"id": "glm-5.2",
"name": "GLM-5.2",
"vendor": "Custom",
"url": "https://api.getaigate.com/v1/chat/completions",
"apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb",
"maxInputTokens": 1000000,
"maxOutputTokens": 128000,
"supportsToolCall": true,
"supportsImages": true,
"supportsReasoning": true,
"useCustomProtocol": false
},
{
"id": "glm-5.1",
"name": "GLM-5.1",
"vendor": "Custom",
"url": "https://api.getaigate.com/v1/chat/completions",
"apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb",
"maxInputTokens": 200000,
"maxOutputTokens": 128000,
"supportsToolCall": true,
"supportsImages": true,
"supportsReasoning": true,
"useCustomProtocol": false
},
{
"id": "glm-5.0",
"name": "GLM-5.0",
"vendor": "Custom",
"url": "https://api.getaigate.com/v1/chat/completions",
"apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb",
"maxInputTokens": 200000,
"maxOutputTokens": 32000,
"supportsToolCall": true,
"supportsImages": true,
"supportsReasoning": true,
"useCustomProtocol": false
},
{
"id": "glm-5.0-turbo",
"name": "GLM-5.0-Turbo",
"vendor": "Custom",
"url": "https://api.getaigate.com/v1/chat/completions",
"apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb",
"maxInputTokens": 200000,
"maxOutputTokens": 32000,
"supportsToolCall": true,
"supportsImages": true,
"supportsReasoning": true,
"useCustomProtocol": false
},
{
"id": "glm-5v-turbo",
"name": "GLM-5v-Turbo",
"vendor": "Custom",
"url": "https://api.getaigate.com/v1/chat/completions",
"apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb",
"maxInputTokens": 200000,
"maxOutputTokens": 32000,
"supportsToolCall": true,
"supportsImages": true,
"supportsReasoning": true,
"useCustomProtocol": false
},
{
"id": "glm-4.7",
"name": "GLM-4.7",
"vendor": "Custom",
"url": "https://api.getaigate.com/v1/chat/completions",
"apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb",
"maxInputTokens": 128000,
"maxOutputTokens": 32000,
"supportsToolCall": true,
"supportsImages": true,
"supportsReasoning": true,
"useCustomProtocol": false
},
{
"id": "glm-4.6",
"name": "GLM-4.6",
"vendor": "Custom",
"url": "https://api.getaigate.com/v1/chat/completions",
"apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb",
"maxInputTokens": 128000,
"maxOutputTokens": 32000,
"supportsToolCall": true,
"supportsImages": true,
"supportsReasoning": true,
"useCustomProtocol": false
},
{
"id": "minimax-m3",
"name": "Minimax-M3",
"vendor": "Custom",
"url": "https://api.getaigate.com/v1/chat/completions",
"apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb",
"maxInputTokens": 1000000,
"maxOutputTokens": 32000,
"supportsToolCall": true,
"supportsImages": true,
"supportsReasoning": true,
"useCustomProtocol": false
},
{
"id": "minimax-m2.7",
"name": "Minimax-M2.7",
"vendor": "Custom",
"url": "https://api.getaigate.com/v1/chat/completions",
"apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb",
"maxInputTokens": 128000,
"maxOutputTokens": 32000,
"supportsToolCall": true,
"supportsImages": true,
"supportsReasoning": true,
"useCustomProtocol": false
},
{
"id": "minimax-m2.5",
"name": "Minimax-M2.5",
"vendor": "Custom",
"url": "https://api.getaigate.com/v1/chat/completions",
"apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb",
"maxInputTokens": 128000,
"maxOutputTokens": 32000,
"supportsToolCall": true,
"supportsImages": true,
"supportsReasoning": true,
"useCustomProtocol": false
},
{
"id": "kimi-k2.7-code",
"name": "Kimi-K2.7-Code",
"vendor": "Custom",
"url": "https://api.getaigate.com/v1/chat/completions",
"apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb",
"maxInputTokens": 256000,
"maxOutputTokens": 20000,
"supportsToolCall": true,
"supportsImages": true,
"supportsReasoning": true,
"useCustomProtocol": false
},
{
"id": "kimi-k2.6",
"name": "Kimi-K2.6",
"vendor": "Custom",
"url": "https://api.getaigate.com/v1/chat/completions",
"apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb",
"maxInputTokens": 256000,
"maxOutputTokens": 32000,
"supportsToolCall": true,
"supportsImages": true,
"supportsReasoning": true,
"useCustomProtocol": false
},
{
"id": "kimi-k2.5",
"name": "Kimi-K2.5",
"vendor": "Custom",
"url": "https://api.getaigate.com/v1/chat/completions",
"apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb",
"maxInputTokens": 256000,
"maxOutputTokens": 32000,
"supportsToolCall": true,
"supportsImages": true,
"supportsReasoning": true,
"useCustomProtocol": false
},
{
"id": "hy3-preview",
"name": "HY-3-Preview",
"vendor": "Custom",
"url": "https://api.getaigate.com/v1/chat/completions",
"apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb",
"maxInputTokens": 200000,
"maxOutputTokens": 32000,
"supportsToolCall": true,
"supportsImages": true,
"supportsReasoning": true,
"useCustomProtocol": false
},
{
"id": "hy3",
"name": "HY-3",
"vendor": "Custom",
"url": "https://api.getaigate.com/v1/chat/completions",
"apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb",
"maxInputTokens": 256000,
"maxOutputTokens": 192000,
"supportsToolCall": true,
"supportsImages": true,
"supportsReasoning": true,
"useCustomProtocol": false
},
{
"id": "hunyuan-chat",
"name": "HY-Chat",
"vendor": "Custom",
"url": "https://api.getaigate.com/v1/chat/completions",
"apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb",
"maxInputTokens": 128000,
"maxOutputTokens": 32000,
"supportsToolCall": true,
"supportsImages": true,
"supportsReasoning": true,
"useCustomProtocol": false
},
{
"id": "hunyuan-2.0-thinking",
"name": "HY-2.0-Thinking",
"vendor": "Custom",
"url": "https://api.getaigate.com/v1/chat/completions",
"apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb",
"maxInputTokens": 200000,
"maxOutputTokens": 32000,
"supportsToolCall": true,
"supportsImages": true,
"supportsReasoning": true,
"useCustomProtocol": false
}
]
}
+37 -55
View File
@@ -1,14 +1,13 @@
{
"name": "template-5",
"name": "websopy-taro",
"version": "1.0.0",
"private": true,
"description": "WebSoft Inc.",
"templateInfo": {
"name": "react-NutUI",
"typescript": true,
"css": "Sass",
"framework": "React"
},
"description": "Taro + React + TypeScript 项目框架",
"browserslist": [
"last 3 versions",
"Android >= 4.1",
"ios >= 8"
],
"scripts": {
"build:weapp": "taro build --type weapp",
"build:swan": "taro build --type swan",
@@ -28,81 +27,64 @@
"dev:qq": "npm run build:qq -- --watch",
"dev:jd": "npm run build:jd -- --watch",
"dev:quickapp": "npm run build:quickapp -- --watch",
"build:tailwind": "postcss --config tailwind.config.js -o ./dist/index.css ./src/app.css"
"lint": "eslint src --fix --ext .ts,.tsx",
"type-check": "tsc --noEmit"
},
"browserslist": [
"last 3 versions",
"Android >= 4.1",
"ios >= 8"
],
"author": "",
"dependencies": {
"@babel/runtime": "^7.26.0",
"@nutui/icons-react-taro": "^2.0.1",
"@nutui/nutui-biz": "1.0.0-beta.2",
"@nutui/nutui-react": "^3.0.16",
"@nutui/nutui-react-taro": "^2.7.4",
"@react-native/metro-config": "^0.73.2",
"@tarojs/components": "4.0.8",
"@tarojs/components-rn": "^4.1.4",
"@tarojs/helper": "4.0.8",
"@tarojs/plugin-framework-react": "4.0.8",
"@tarojs/plugin-html": "4.0.8",
"@tarojs/plugin-platform-alipay": "4.0.8",
"@nutui/nutui-react-taro": "2.7.4",
"@tarojs/components": "4.1.11",
"@tarojs/plugin-framework-react": "4.1.11",
"@tarojs/plugin-platform-h5": "4.0.8",
"@tarojs/plugin-platform-jd": "4.0.8",
"@tarojs/plugin-platform-qq": "4.0.8",
"@tarojs/plugin-platform-swan": "4.0.8",
"@tarojs/plugin-platform-tt": "4.0.8",
"@tarojs/plugin-platform-weapp": "4.0.8",
"@tarojs/react": "4.0.8",
"@tarojs/rn-runner": "^4.1.4",
"@tarojs/rn-supporter": "^4.1.4",
"@tarojs/runtime": "4.0.8",
"@tarojs/runtime-rn": "^4.1.4",
"@tarojs/shared": "4.0.8",
"@tarojs/taro": "4.0.8",
"@tarojs/taro-rn": "^4.1.4",
"@tarojs/plugin-platform-weapp": "4.1.11",
"@tarojs/react": "4.1.11",
"@tarojs/runtime": "4.1.11",
"@tarojs/shared": "4.1.11",
"@tarojs/taro": "4.1.11",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.13",
"echarts-taro3-react": "^1.0.13",
"expo": "~50.0.2",
"js-base64": "^3.7.7",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-markdown": "^10.1.0",
"react-native": "^0.73.1",
"react-router-dom": "^7.1.1",
"weapp-qrcode": "^1.0.0"
},
"devDependencies": {
"@babel/core": "^7.26.0",
"@babel/plugin-proposal-class-properties": "7.14.5",
"@babel/preset-react": "^7.26.3",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/preset-react": "^7.26.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
"@tarojs/cli": "4.0.8",
"@tarojs/taro-loader": "4.0.8",
"@tarojs/webpack5-runner": "4.0.8",
"@tarojs/cli": "4.1.11",
"@tarojs/taro-loader": "4.1.11",
"@tarojs/webpack5-runner": "4.1.11",
"@types/node": "^18.19.68",
"@types/react": "^18.3.18",
"@types/react": "^18.3.12",
"@types/webpack-env": "^1.18.5",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@typescript-eslint/eslint-plugin": "^8.15.0",
"@typescript-eslint/parser": "^8.15.0",
"autoprefixer": "^10.4.20",
"babel-plugin-import": "^1.13.8",
"babel-preset-taro": "4.0.8",
"eslint": "^8.57.1",
"eslint": "^8.56.0",
"eslint-config-taro": "4.0.8",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-react": "^7.37.3",
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-hooks": "^4.6.2",
"mysql2": "^3.22.3",
"postcss": "^8.4.49",
"react-refresh": "^0.11.0",
"react-refresh": "^0.14.0",
"stylelint": "^14.16.1",
"tailwindcss": "^3.4.17",
"tailwindcss": "3.4.17",
"ts-node": "^10.9.2",
"tsconfig-paths-webpack-plugin": "^4.2.0",
"typescript": "^5.7.2",
"typescript": "5.7.2",
"weapp-tailwindcss": "^4.12.0",
"webpack": "5.78.0"
},
"engines": {
"node": ">=18"
}
}
+5859 -10189
View File
File diff suppressed because it is too large Load Diff
-46
View File
@@ -1,46 +0,0 @@
/**
* PostCSS 插件:移除 WXSS 不兼容的 Tailwind CSS 规则
*
* 微信小程序 WXSS 不支持以下 CSS 特性:
* 1. 反斜杠转义的特殊字符(如 .\!visible)—— !important 修饰符
* 2. :not() 伪类 + ~ 兄弟选择器组合(如 .space-y-4 > :not([hidden]) ~ :not([hidden]))
* 3. CSS 自定义属性 / CSS 变量(如 --tw-space-y-reverse)
* 4. 嵌套 calc()(如 calc(0.5rem * calc(1 - var(--tw-space-y-reverse))))
*
* 该插件移除以下类型的 Tailwind CSS 规则:
* - .\!xxx 类选择器(important 修饰符)
* - .space-[x|y]-* 规则(间距布局,依赖 :not + ~ + CSS 变量 + 嵌套 calc)
* - .divide-[x|y]-* 规则(分割线布局,同样依赖不兼容特性)
*/
/** @type {import('postcss').PluginCreator} */
module.exports = (options = {}) => {
return {
postcssPlugin: 'postcss-remove-wxss-incompatible',
Rule(rule) {
const selector = rule.selector
// 1) 移除 .\!xxx 类选择器(Tailwind important 修饰符,反斜杠转义的 ! 不兼容 WXSS)
const selectors = selector.split(',')
const hasEscapedImportant = selectors.some(s => /\.\\!/.test(s.trim()))
if (hasEscapedImportant) {
rule.remove()
return
}
// 2) 移除 .space-[x|y]-* 规则(依赖 :not + ~ 兄弟组合器,WXSS 不兼容)
if (/\.space-[xy]-/.test(selector)) {
rule.remove()
return
}
// 3) 移除 .divide-[x|y]-* 规则(同上,依赖 :not + ~)
if (/\.divide-[xy]-/.test(selector)) {
rule.remove()
return
}
}
}
}
module.exports.postcss = true
+5 -10
View File
@@ -1,12 +1,7 @@
const wxssIncompatiblePlugin = require('./postcss-remove-wxss-incompatible')
module.exports = {
plugins: [
require('tailwindcss'),
require('autoprefixer'),
require('postcss-nested'),
// 移除 WXSS 不兼容的 Tailwind CSS 规则
// 包括 .\!xxx(反斜杠转义 !)、space-x/y-*(:not + ~ 组合器)、divide-x/y-* 等
wxssIncompatiblePlugin()
]
plugins: {
tailwindcss: {},
autoprefixer: {},
'postcss-nested': {}
}
}
+34 -27
View File
@@ -1,48 +1,55 @@
{
"miniprogramRoot": "dist/",
"projectname": "template-5",
"description": "网宿软件",
"appid": "wx541db955e7a62709",
"description": "websopy-taro 项目配置",
"setting": {
"urlCheck": true,
"es6": false,
"enhance": false,
"compileHotReLoad": false,
"postcss": false,
"urlCheck": false,
"es6": true,
"enhance": true,
"postcss": true,
"preloadBackgroundData": false,
"minified": true,
"newFeature": true,
"autoAudits": false,
"coverView": true,
"showShadowRootInWxmlPanel": false,
"nodeModules": false,
"autoAudits": false,
"showShadowRootInWxmlPanel": true,
"scopeDataCheck": false,
"useCompilerModule": false,
"compileWorklet": false,
"uglifyFileName": false,
"checkInvalidKey": true,
"checkSiteMap": true,
"uploadWithSourceMap": true,
"packNpmManually": false,
"packNpmRelationList": [],
"minifyWXSS": true,
"minifyWXML": true,
"localPlugins": false,
"disableUseStrict": false,
"useCompilerPlugins": false,
"condition": false,
"swc": false,
"disableSWC": true,
"compileHotReLoad": true,
"lazyloadPlaceholderEnable": false,
"useMultiFrameRuntime": true,
"useApiHook": true,
"useApiHostProcess": true,
"babelSetting": {
"ignore": [],
"disablePlugins": [],
"outputPath": ""
}
},
"useIsolateContext": true,
"userConfirmedBundleSwitch": false,
"packNpmManually": false,
"packNpmRelationList": [],
"minifyWXSS": true,
"disableUseStrict": false,
"minifyWXML": true,
"showES6CompileOption": false,
"useCompilerPlugins": false,
"compileWorklet": false,
"localPlugins": false,
"condition": false,
"swc": false,
"disableSWC": true
},
"compileType": "miniprogram",
"simulatorType": "wechat",
"simulatorPluginLibVersion": {},
"appid": "wx541db955e7a62709",
"projectname": "威硕谱软件",
"condition": {},
"simulatorPluginLibVersion": {},
"packOptions": {
"ignore": [],
"include": []
},
"editorSetting": {}
}
}
+8 -61
View File
@@ -1,76 +1,23 @@
{
"libVersion": "3.15.2",
"projectname": "websopy-mp",
"condition": {
"miniprogram": {
"list": [
{
"name": "passport/webview/index",
"pathName": "passport/webview/index",
"query": "url=https%3A%2F%2Fwebsopy.websoft.top%2Fdeveloper",
"scene": null,
"launchMode": "default"
},
{
"name": "passport/login",
"pathName": "passport/login",
"query": "",
"launchMode": "default",
"scene": null
},
{
"name": "passport/invite/index",
"pathName": "passport/invite/index",
"query": "",
"launchMode": "default",
"scene": null
},
{
"name": "passport/invite/index",
"pathName": "passport/invite/index",
"query": "",
"launchMode": "default",
"scene": null
},
{
"name": "passport/invite",
"pathName": "passport/invite",
"query": "",
"launchMode": "default",
"scene": null
},
{
"name": "pages/passport/qr-confirm/index",
"pathName": "pages/passport/qr-confirm/index",
"query": "",
"launchMode": "default",
"scene": null
},
{
"name": "passport/qr-confirm/index",
"pathName": "passport/qr-confirm/index",
"query": "",
"launchMode": "default",
"scene": null
}
]
}
},
"libVersion": "3.16.1",
"projectname": "websopy-taro",
"condition": {},
"setting": {
"urlCheck": true,
"urlCheck": false,
"coverView": true,
"lazyloadPlaceholderEnable": false,
"skylineRenderEnable": false,
"preloadBackgroundData": false,
"autoAudits": false,
"useApiHook": true,
"showShadowRootInWxmlPanel": false,
"showShadowRootInWxmlPanel": true,
"useStaticServer": false,
"useLanDebug": false,
"showES6CompileOption": false,
"compileHotReLoad": true,
"checkInvalidKey": true,
"ignoreDevUnusedFiles": true,
"bigPackageSizeSupport": false
"bigPackageSizeSupport": false,
"useIsolateContext": true
}
}
}
-13
View File
@@ -1,13 +0,0 @@
{
"miniprogramRoot": "./",
"projectname": "mp-react",
"description": "网宿软件",
"appid": "touristappid",
"setting": {
"urlCheck": true,
"es6": false,
"postcss": false,
"minified": false
},
"compileType": "miniprogram"
}
-4
View File
@@ -1,4 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '新增收货地址',
navigationBarTextStyle: 'black'
})
-323
View File
@@ -1,323 +0,0 @@
import {useEffect, useState, useRef} from "react";
import {useRouter} from '@tarojs/taro'
import {Button, Loading, CellGroup, Input, TextArea, Form, Switch, InputNumber, Radio, Image} from '@nutui/nutui-react-taro'
import {Edit, Upload as UploadIcon} from '@nutui/icons-react-taro'
import Taro from '@tarojs/taro'
import {View} from '@tarojs/components'
import {ShopArticle} from "@/api/shop/shopArticle/model";
import {getShopArticle, addShopArticle, updateShopArticle} from "@/api/shop/shopArticle";
import FixedButton from "@/components/FixedButton";
const AddShopArticle = () => {
const {params} = useRouter();
const [loading, setLoading] = useState<boolean>(true)
const [formData, setFormData] = useState<ShopArticle>({
type: 0, // 默认常规文章
status: 0, // 默认已发布
permission: 0, // 默认所有人可见
recommend: 0, // 默认不推荐
showType: 10, // 默认小图展示
virtualViews: 0, // 默认虚拟阅读量
actualViews: 0, // 默认实际阅读量
sortNumber: 0 // 默认排序
})
const formRef = useRef<any>(null)
// 判断是编辑还是新增模式
const isEditMode = !!params.id
const articleId = params.id ? Number(params.id) : undefined
// 文章类型选项
const typeOptions = [
{ text: '常规文章', value: 0 },
{ text: '视频文章', value: 1 }
]
// 状态选项
const statusOptions = [
{ text: '已发布', value: 0 },
{ text: '待审核', value: 1 },
{ text: '已驳回', value: 2 },
{ text: '违规内容', value: 3 }
]
// 可见性选项
const permissionOptions = [
{ text: '所有人可见', value: 0 },
{ text: '登录可见', value: 1 },
{ text: '密码可见', value: 2 }
]
// 显示方式选项
const showTypeOptions = [
{ text: '小图展示', value: 10 },
{ text: '大图展示', value: 20 }
]
const reload = async () => {
// 如果是编辑模式,加载文章数据
if (isEditMode && articleId) {
try {
const article = await getShopArticle(articleId)
setFormData(article)
// 更新表单值
if (formRef.current) {
formRef.current.setFieldsValue(article)
}
} catch (error) {
console.error('加载文章失败:', error)
Taro.showToast({
title: '加载文章失败',
icon: 'error'
});
}
}
}
// 图片上传处理
const handleImageUpload = async () => {
try {
const res = await Taro.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera']
});
if (res.tempFilePaths && res.tempFilePaths.length > 0) {
// 这里应该调用上传接口,暂时使用本地路径
const imagePath = res.tempFilePaths[0];
setFormData({
...formData,
image: imagePath
});
Taro.showToast({
title: '图片选择成功',
icon: 'success'
});
}
} catch (error) {
Taro.showToast({
title: '图片选择失败',
icon: 'error'
});
}
};
// 提交表单
const submitSucceed = async (values: any) => {
try {
// 准备提交的数据
const submitData = {
...formData,
...values,
};
// 如果是编辑模式,添加id
if (isEditMode && articleId) {
submitData.articleId = articleId;
}
// 执行新增或更新操作
if (isEditMode) {
await updateShopArticle(submitData);
} else {
await addShopArticle(submitData);
}
Taro.showToast({
title: `${isEditMode ? '更新' : '保存'}成功`,
icon: 'success'
});
setTimeout(() => {
Taro.navigateBack();
}, 1000);
} catch (error) {
console.error('保存失败:', error);
Taro.showToast({
title: `${isEditMode ? '更新' : '保存'}失败`,
icon: 'error'
});
}
}
const submitFailed = (error: any) => {
console.log(error, 'err...')
}
useEffect(() => {
// 动态设置页面标题
Taro.setNavigationBarTitle({
title: isEditMode ? '编辑文章' : '新增文章'
});
reload().then(() => {
setLoading(false)
})
}, [isEditMode]);
if (loading) {
return <Loading className={'px-2'}>加载中</Loading>
}
return (
<>
<Form
ref={formRef}
divider
initialValues={formData}
labelPosition="left"
onFinish={(values) => submitSucceed(values)}
onFinishFailed={(errors) => submitFailed(errors)}
>
{/* 基本信息 */}
<CellGroup title="基本信息">
<Form.Item
name="title"
label="文章标题"
required
rules={[{ required: true, message: '请输入文章标题' }]}
initialValue={formData.title}
>
<Input placeholder="请输入文章标题" maxLength={100}/>
</Form.Item>
<Form.Item name="overview" label="文章概述" initialValue={formData.overview}>
<TextArea placeholder="请输入文章概述,用于列表展示" maxLength={200} rows={3}/>
</Form.Item>
<Form.Item
name="detail"
label="文章内容"
required
rules={[{ required: true, message: '请输入文章内容' }]}
initialValue={formData.detail}
>
<TextArea placeholder="请输入文章内容" maxLength={10000} rows={8}/>
</Form.Item>
<Form.Item name="author" label="作者" initialValue={formData.author}>
<Input placeholder="请输入作者名称" maxLength={50}/>
</Form.Item>
<Form.Item name="source" label="来源" initialValue={formData.source}>
<Input placeholder="请输入文章来源" maxLength={100}/>
</Form.Item>
</CellGroup>
{/* 文章设置 */}
<CellGroup title="文章设置">
<Form.Item name="type" label="文章类型" initialValue={formData.type}>
<Radio.Group direction="horizontal" value={formData.type}>
{typeOptions.map(option => (
<Radio key={option.value} value={option.value}>
{option.text}
</Radio>
))}
</Radio.Group>
</Form.Item>
<Form.Item name="status" label="发布状态" initialValue={formData.status}>
<Radio.Group direction="horizontal" value={formData.status}>
{statusOptions.map(option => (
<Radio key={option.value} value={option.value}>
{option.text}
</Radio>
))}
</Radio.Group>
</Form.Item>
<Form.Item name="permission" label="可见性" initialValue={formData.permission}>
<Radio.Group direction="horizontal" value={formData.permission}>
{permissionOptions.map(option => (
<Radio key={option.value} value={option.value}>
{option.text}
</Radio>
))}
</Radio.Group>
</Form.Item>
<Form.Item name="showType" label="显示方式" initialValue={formData.showType}>
<Radio.Group direction="horizontal" value={formData.showType}>
{showTypeOptions.map(option => (
<Radio key={option.value} value={option.value}>
{option.text}
</Radio>
))}
</Radio.Group>
</Form.Item>
</CellGroup>
{/* 高级设置 */}
<CellGroup title="高级设置">
<Form.Item name="recommend" label="推荐文章" initialValue={formData.recommend}>
<Switch
checked={formData.recommend === 1}
onChange={(checked) =>
setFormData({...formData, recommend: checked ? 1 : 0})
}
/>
</Form.Item>
<Form.Item name="price" label="付费金额" initialValue={formData.price}>
<Input placeholder="0.00" type="number"/>
<View className="text-xs text-gray-500 mt-1">单位:元</View>
</Form.Item>
<Form.Item name="virtualViews" label="虚拟阅读量" initialValue={formData.virtualViews}>
<InputNumber min={0} defaultValue={formData.virtualViews || 0}/>
</Form.Item>
<Form.Item name="actualViews" label="实际阅读量" initialValue={formData.actualViews}>
<InputNumber min={0} defaultValue={formData.actualViews || 0}/>
</Form.Item>
<Form.Item name="sortNumber" label="排序" initialValue={formData.sortNumber}>
<InputNumber min={0} defaultValue={formData.sortNumber || 0}/>
<View className="text-xs text-gray-500 mt-1">数字越小越靠前</View>
</Form.Item>
<Form.Item name="tags" label="标签" initialValue={formData.tags}>
<Input placeholder="请输入标签,多个标签用逗号分隔" maxLength={200}/>
</Form.Item>
<Form.Item name="topic" label="话题" initialValue={formData.topic}>
<Input placeholder="请输入话题" maxLength={100}/>
</Form.Item>
</CellGroup>
{/* 图片上传 */}
<CellGroup title="文章图片">
<Form.Item name="image" label="封面图片" initialValue={formData.image}>
<View className="flex items-center gap-3">
{formData.image && (
<Image
src={formData.image}
width="80"
height="80"
radius="8"
/>
)}
<Button
size="small"
type="primary"
fill="outline"
icon={<UploadIcon />}
onClick={handleImageUpload}
>
{formData.image ? '更换图片' : '上传图片'}
</Button>
</View>
</Form.Item>
</CellGroup>
{/* 提交按钮 */}
<FixedButton text={isEditMode ? '更新文章' : '发布文章'} onClick={() => submitSucceed} icon={<Edit />} />
</Form>
</>
);
};
export default AddShopArticle;
-4
View File
@@ -1,4 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '商品文章管理',
navigationBarTextStyle: 'black'
})
-271
View File
@@ -1,271 +0,0 @@
import {useState} from "react";
import Taro, {useDidShow} from '@tarojs/taro'
import {Button, Cell, CellGroup, Empty, ConfigProvider, SearchBar, Tag, InfiniteLoading, Loading, PullToRefresh} from '@nutui/nutui-react-taro'
import {Edit, Del, Eye} from '@nutui/icons-react-taro'
import {View} from '@tarojs/components'
import {CmsArticle} from "@/api/cms/cmsArticle/model";
import {pageCmsArticle, removeCmsArticle} from "@/api/cms/cmsArticle";
import FixedButton from "@/components/FixedButton";
import dayjs from "dayjs";
const ArticleArticleManage = () => {
const [list, setList] = useState<CmsArticle[]>([])
const [loading, setLoading] = useState(false)
// const [refreshing, setRefreshing] = useState(false)
const [hasMore, setHasMore] = useState(true)
const [searchValue, setSearchValue] = useState('')
const [page, setPage] = useState(1)
const [total, setTotal] = useState(0)
const reload = async (isRefresh = false) => {
if (isRefresh) {
setPage(1)
setList([])
setHasMore(true)
}
setLoading(true)
try {
const currentPage = isRefresh ? 1 : page
const res = await pageCmsArticle({
page: currentPage,
limit: 10,
keywords: searchValue
})
if (res && res.list) {
const newList = isRefresh ? res.list : [...list, ...res.list]
setList(newList)
setTotal(res.count || 0)
// 判断是否还有更多数据
setHasMore(res.list.length === 10) // 如果返回的数据等于limit,说明可能还有更多
if (!isRefresh) {
setPage(currentPage + 1)
} else {
setPage(2) // 刷新后下一页是第2页
}
} else {
setHasMore(false)
setTotal(0)
}
} catch (error) {
console.error('获取文章失败:', error)
Taro.showToast({
title: '获取文章失败',
icon: 'error'
});
} finally {
setLoading(false)
}
}
// 搜索功能
const handleSearch = (value: string) => {
setSearchValue(value)
reload(true)
}
// 下拉刷新
const handleRefresh = async () => {
// setRefreshing(true)
await reload(true)
// setRefreshing(false)
}
// 删除文章
const handleDelete = async (id?: number) => {
Taro.showModal({
title: '确认删除',
content: '确定要删除这篇文章吗?',
success: async (res) => {
if (res.confirm) {
try {
await removeCmsArticle(id)
Taro.showToast({
title: '删除成功',
icon: 'success'
});
reload(true);
} catch (error) {
Taro.showToast({
title: '删除失败',
icon: 'error'
});
}
}
}
});
}
// 编辑文章
const handleEdit = (item: CmsArticle) => {
Taro.navigateTo({
url: `/shop/shopArticle/add?id=${item.articleId}`
});
}
// 查看文章详情
const handleView = (item: CmsArticle) => {
// 这里可以跳转到文章详情页面
Taro.navigateTo({
url: `/cms/detail/index?id=${item.articleId}`
})
}
// 获取状态标签
const getStatusTag = (status?: number) => {
switch (status) {
case 0:
return <Tag type="success">已发布</Tag>
case 1:
return <Tag type="warning">待审核</Tag>
case 2:
return <Tag type="danger">已驳回</Tag>
case 3:
return <Tag type="danger">违规内容</Tag>
default:
return <Tag>未知</Tag>
}
}
// 加载更多
const loadMore = async () => {
if (!loading && hasMore) {
await reload(false) // 不刷新,追加数据
}
}
useDidShow(() => {
reload(true).then()
});
return (
<ConfigProvider>
{/* 搜索栏 */}
<View className="py-2">
<SearchBar
placeholder="搜索关键词"
value={searchValue}
onChange={setSearchValue}
onSearch={handleSearch}
/>
</View>
{/* 统计信息 */}
{total > 0 && (
<View className="px-4 py-2 text-sm text-gray-500">
共找到 {total} 篇文章
</View>
)}
{/* 文章列表 */}
<PullToRefresh
onRefresh={handleRefresh}
headHeight={60}
>
<View className="px-4" style={{ height: 'calc(100vh - 160px)', overflowY: 'auto' }} id="article-scroll">
{list.length === 0 && !loading ? (
<View className="flex flex-col justify-center items-center" style={{height: 'calc(100vh - 200px)'}}>
<Empty
description="暂无文章数据"
style={{backgroundColor: 'transparent'}}
/>
</View>
) : (
<InfiniteLoading
target="article-scroll"
hasMore={hasMore}
onLoadMore={loadMore}
loadingText={
<View className="flex justify-center items-center py-4">
<Loading />
<View className="ml-2">加载中...</View>
</View>
}
loadMoreText={
<View className="text-center py-4 text-gray-500">
{list.length === 0 ? "暂无数据" : "没有更多了"}
</View>
}
>
{list.map((item, index) => (
<CellGroup key={item.articleId || index} className="mb-4">
<Cell>
<View className="flex flex-col gap-3 w-full">
{/* 文章标题和状态 */}
<View className="flex justify-between items-start">
<View className="flex-1 pr-2">
<View className="text-lg font-bold text-gray-900 line-clamp-2">
{item.title}
</View>
</View>
{getStatusTag(item.status)}
</View>
{/* 文章概述 */}
{item.overview && (
<View className="text-sm text-gray-600 line-clamp-2">
{item.overview}
</View>
)}
{/* 文章信息 */}
<View className="flex justify-between items-center text-xs text-gray-500">
<View className="flex items-center gap-4">
<View>阅读: {item.actualViews || 0}</View>
{item.price && <View>价格: ¥{item.price}</View>}
<View>创建: {dayjs(item.createTime).format('MM-DD HH:mm')}</View>
</View>
</View>
{/* 操作按钮 */}
<View className="flex justify-end gap-2 pt-2 border-t border-gray-100">
<Button
size="small"
fill="outline"
icon={<Eye/>}
onClick={() => handleView(item)}
>
查看
</Button>
<Button
size="small"
fill="outline"
icon={<Edit/>}
onClick={() => handleEdit(item)}
>
编辑
</Button>
<Button
size="small"
type="danger"
fill="outline"
icon={<Del/>}
onClick={() => handleDelete(item.articleId)}
>
删除
</Button>
</View>
</View>
</Cell>
</CellGroup>
))}
</InfiniteLoading>
)}
</View>
</PullToRefresh>
{/* 底部浮动按钮 */}
<FixedButton
text="发布文章"
icon={<Edit />}
onClick={() => Taro.navigateTo({url: '/shop/shopArticle/add'})}
/>
</ConfigProvider>
);
};
export default ArticleArticleManage;
-255
View File
@@ -1,255 +0,0 @@
import {Button} from '@nutui/nutui-react-taro'
import {Avatar, Tag} from '@nutui/nutui-react-taro'
import {getUserInfo, getWxOpenId} from '@/api/layout';
import Taro from '@tarojs/taro';
import {useEffect, useState} from "react";
import {User} from "@/api/system/user/model";
import navTo from "@/utils/common";
import {TenantId} from "@/config/app";
import {getMyAvailableCoupons} from "@/api/shop/shopUserCoupon";
import {useUser} from "@/hooks/useUser";
import {getStoredInviteParams} from "@/utils/invite";
function UserCard() {
const {getDisplayName, getRoleName} = useUser();
const [IsLogin, setIsLogin] = useState<boolean>(false)
const [userInfo, setUserInfo] = useState<User>()
const [couponCount, setCouponCount] = useState(0)
// const [pointsCount, setPointsCount] = useState(0)
const [giftCount, setGiftCount] = useState(0)
useEffect(() => {
// Taro.getSetting:获取用户的当前设置。返回值中只会出现小程序已经向用户请求过的权限。
Taro.getSetting({
success: (res) => {
if (res.authSetting['scope.userInfo']) {
// 用户已经授权过,可以直接获取用户信息
console.log('用户已经授权过,可以直接获取用户信息')
reload();
} else {
// 用户未授权,需要弹出授权窗口
console.log('用户未授权,需要弹出授权窗口')
showAuthModal();
}
}
});
}, []);
const loadUserStats = (userId: number) => {
// 加载优惠券数量
getMyAvailableCoupons()
.then((coupons: any) => {
setCouponCount(coupons?.length || 0)
})
.catch((error: any) => {
console.error('Coupon count error:', error)
})
// 加载积分数量
console.log(userId)
// getUserPointsStats(userId)
// .then((res: any) => {
// setPointsCount(res.currentPoints || 0)
// })
// .catch((error: any) => {
// console.error('Points stats error:', error)
// })
// 加载礼品劵数量
setGiftCount(0)
// pageUserGiftLog({userId, page: 1, limit: 1}).then(res => {
// setGiftCount(res.count || 0)
// })
}
const reload = () => {
Taro.getUserInfo({
success: (res) => {
const avatar = res.userInfo.avatarUrl;
setUserInfo({
avatar,
nickname: res.userInfo.nickName,
sexName: res.userInfo.gender == 1 ? '男' : '女'
})
getUserInfo().then((data) => {
if (data) {
setUserInfo(data)
setIsLogin(true);
Taro.setStorageSync('UserId', data.userId)
// 加载用户统计数据
if (data.userId) {
loadUserStats(data.userId)
}
// 获取openId
if (!data.openid) {
Taro.login({
success: (res) => {
getWxOpenId({code: res.code}).then(() => {
})
}
})
}
}
}).catch(() => {
console.log('未登录')
});
}
});
};
const showAuthModal = () => {
Taro.showModal({
title: '授权提示',
content: '需要获取您的用户信息',
confirmText: '去授权',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
// 用户点击确认,打开授权设置页面
openSetting();
}
}
});
};
const openSetting = () => {
// Taro.openSetting:调起客户端小程序设置界面,返回用户设置的操作结果。设置界面只会出现小程序已经向用户请求过的权限。
Taro.openSetting({
success: (res) => {
if (res.authSetting['scope.userInfo']) {
// 用户授权成功,可以获取用户信息
reload();
} else {
// 用户拒绝授权,提示授权失败
Taro.showToast({
title: '授权失败',
icon: 'none'
});
}
}
});
};
/* 获取用户手机号 */
const handleGetPhoneNumber = ({detail}: {detail: {code?: string, encryptedData?: string, iv?: string}}) => {
const {code, encryptedData, iv} = detail
// 获取存储的邀请参数
const inviteParams = getStoredInviteParams()
const refereeId = inviteParams?.inviter ? parseInt(inviteParams.inviter) : 0
Taro.login({
success: function () {
if (code) {
Taro.request({
url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone',
method: 'POST',
data: {
code,
encryptedData,
iv,
notVerifyPhone: true,
refereeId: refereeId, // 使用解析出的推荐人ID
sceneType: 'save_referee',
tenantId: TenantId
},
header: {
'content-type': 'application/json',
TenantId
},
success: function (res) {
if (res.data.code == 1) {
Taro.showToast({
title: res.data.message,
icon: 'error',
duration: 2000
})
return false;
}
// 登录成功
Taro.setStorageSync('access_token', res.data.data.access_token)
Taro.setStorageSync('UserId', res.data.data.user.userId)
setUserInfo(res.data.data.user)
setIsLogin(true)
}
})
} else {
console.log('登录失败!')
}
}
})
}
return (
<div className={'header-bg pt-20'}>
<div className={'p-4'}>
<div
className={'user-card w-full flex flex-col justify-around rounded-xl shadow-sm'}
style={{
background: 'linear-gradient(to bottom, #ffffff, #ffffff)', // 这种情况建议使用类名来控制样式(引入外联样式)
// width: '720rpx',
// margin: '10px auto 0px auto',
height: '170px',
// borderRadius: '22px 22px 0 0',
}}
>
<div className={'user-card-header flex w-full justify-between items-center pt-4'}>
<div className={'flex items-center mx-4'}>
{
IsLogin ? (
<Avatar size="large" src={userInfo?.avatar} shape="round"/>
) : (
<Button className={'text-black'} open-type="getPhoneNumber" onGetPhoneNumber={handleGetPhoneNumber}>
<Avatar size="large" src={userInfo?.avatar} shape="round"/>
</Button>
)
}
<div className={'user-info flex flex-col px-2'}>
<div className={'py-1 text-black font-bold'}>{getDisplayName()}</div>
{IsLogin ? (
<div className={'grade text-xs py-1'}>
<Tag type="success" round>
<div className={'p-1'}>
{getRoleName()}
</div>
</Tag>
</div>
) : ''}
</div>
</div>
<div className={'mx-4 text-sm px-3 py-1 text-black border-gray-400 border-solid border-2 rounded-3xl'}
onClick={() => navTo('/user/profile/profile', true)}>
{'个人资料'}
</div>
</div>
<div className={'flex justify-around mt-1'}>
<div className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/wallet/wallet', true)}>
<span className={'text-sm text-gray-500'}>余额</span>
<span className={'text-xl'}>¥ {userInfo?.balance || '0.00'}</span>
</div>
<div className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/coupon/index', true)}>
<span className={'text-sm text-gray-500'}>优惠券</span>
<span className={'text-xl'}>{couponCount}</span>
</div>
<div className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/gift/index', true)}>
<span className={'text-sm text-gray-500'}>水票</span>
<span className={'text-xl'}>{giftCount}</span>
</div>
{/*<div className={'item flex justify-center flex-col items-center'}>*/}
{/* <span className={'text-sm text-gray-500'}>积分</span>*/}
{/* <span className={'text-xl'}>{pointsCount}</span>*/}
{/*</div>*/}
</div>
</div>
</div>
</div>
)
}
export default UserCard;
-186
View File
@@ -1,186 +0,0 @@
import {Cell} from '@nutui/nutui-react-taro'
import navTo from "@/utils/common";
import Taro from '@tarojs/taro'
import {View, Text} from '@tarojs/components'
import {ArrowRight, ShieldCheck, LogisticsError, Location, Reward, Tips, Ask, Setting, Scan} from '@nutui/icons-react-taro'
import {useUser} from '@/hooks/useUser'
const UserCell = () => {
const {logoutUser, isCertified, hasRole, isAdmin} = useUser();
const onLogout = () => {
Taro.showModal({
title: '提示',
content: '确定要退出登录吗?',
success: function (res) {
if (res.confirm) {
// 使用 useUser hook 的 logoutUser 方法
logoutUser();
Taro.reLaunch({
url: '/pages/index/index'
})
}
}
})
}
return (
<>
<View className={'px-4'}>
{/*是否分销商*/}
{!hasRole('dealer') && !isAdmin() && (
<Cell
className="nutui-cell-clickable"
style={{
backgroundImage: 'linear-gradient(to right bottom, #54a799, #177b73)',
}}
title={
<View style={{display: 'inline-flex', alignItems: 'center'}} onClick={() => navTo('/dealer/index', true)}>
<Reward className={'text-orange-100 '} size={16}/>
<Text style={{fontSize: '16px'}} className={'pl-3 text-orange-100 font-medium'}>开通会员</Text>
<Text className={'text-white opacity-80 pl-3'}>享优惠</Text>
</View>
}
extra={<ArrowRight color="#cccccc" size={18}/>}
/>
)}
{/*是否管理员*/}
{isAdmin() && (
<Cell
className="nutui-cell-clickable"
style={{
backgroundImage: 'linear-gradient(to right bottom, #ff8e0c, #ed680d)',
}}
title={
<View style={{display: 'inline-flex', alignItems: 'center'}} onClick={() => navTo('/admin/article/index', true)}>
<Setting className={'text-orange-100 '} size={16}/>
<Text style={{fontSize: '16px'}} className={'pl-3 text-orange-100 font-medium'}>管理中心</Text>
</View>
}
extra={<ArrowRight color="#cccccc" size={18}/>}
/>
)}
<Cell.Group divider={true} description={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<Text style={{marginTop: '12px'}}>我的服务</Text>
</View>
}>
<Cell
className="nutui-cell-clickable"
title={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<Scan size={16}/>
<Text className={'pl-3 text-sm'}>门店核销</Text>
</View>
}
align="center"
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => {
navTo('/user/wallet/index', true)
}}
/>
<Cell
className="nutui-cell-clickable"
style={{
display: 'none'
}}
title={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<LogisticsError size={16}/>
<Text className={'pl-3 text-sm'}>我的钱包</Text>
</View>
}
align="center"
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => {
navTo('/user/wallet/index', true)
}}
/>
<Cell
className="nutui-cell-clickable"
title={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<Location size={16}/>
<Text className={'pl-3 text-sm'}>收货地址</Text>
</View>
}
align="center"
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => {
navTo('/user/address/index', true)
}}
/>
<Cell
className="nutui-cell-clickable"
title={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<ShieldCheck size={16} color={isCertified() ? '#52c41a' : '#666'}/>
<Text className={'pl-3 text-sm'}>实名认证</Text>
{isCertified() && (
<Text className={'pl-2 text-xs text-green-500'}>已认证</Text>
)}
</View>
}
align="center"
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => {
navTo('/user/userVerify/index', true)
}}
/>
<Cell
className="nutui-cell-clickable"
title={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<Ask size={16}/>
<Text className={'pl-3 text-sm'}>常见问题</Text>
</View>
}
align="center"
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => {
navTo('/user/help/index')
}}
/>
<Cell
className="nutui-cell-clickable"
title={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<Tips size={16}/>
<Text className={'pl-3 text-sm'}>关于我们</Text>
</View>
}
align="center"
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => {
navTo('/user/about/index')
}}
/>
</Cell.Group>
<Cell.Group divider={true} description={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<Text style={{marginTop: '12px'}}>账号管理</Text>
</View>
}>
<Cell
className="nutui-cell-clickable"
title="账号安全"
align="center"
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => navTo('/user/profile/profile', true)}
/>
<Cell
className="nutui-cell-clickable"
title="退出登录"
align="center"
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={onLogout}
/>
</Cell.Group>
</View>
</>
)
}
export default UserCell
-102
View File
@@ -1,102 +0,0 @@
import {loginBySms} from "@/api/passport/login";
import {useState} from "react";
import Taro from '@tarojs/taro'
import {Popup} from '@nutui/nutui-react-taro'
import {UserParam} from "@/api/system/user/model";
import {Button} from '@nutui/nutui-react-taro'
import {Form, Input} from '@nutui/nutui-react-taro'
import {Copyright, Version} from "@/config/app";
const UserFooter = () => {
const [openLoginByPhone, setOpenLoginByPhone] = useState(false)
const [clickNum, setClickNum] = useState<number>(0)
const [FormData, setFormData] = useState<UserParam>(
{
phone: undefined,
password: undefined
}
)
const onLoginByPhone = () => {
setFormData({})
setClickNum(clickNum + 1);
if (clickNum > 10) {
setOpenLoginByPhone(true);
}
}
const closeLoginByPhone = () => {
setClickNum(0)
setOpenLoginByPhone(false)
}
// 提交表单
const submitByPhone = (values: any) => {
loginBySms({
phone: values.phone,
code: values.code
}).then(() => {
setOpenLoginByPhone(false);
setTimeout(() => {
Taro.reLaunch({
url: '/pages/index/index'
})
},1000)
})
}
return (
<>
<div className={'text-center py-4 w-full text-gray-300'} onClick={onLoginByPhone}>
<div className={'text-xs text-gray-400 py-1'}>当前版本:{Version}</div>
<div className={'text-xs text-gray-400 py-1'}>Copyright © { new Date().getFullYear() } {Copyright}</div>
</div>
<Popup
style={{width: '350px', padding: '10px'}}
visible={openLoginByPhone}
closeOnOverlayClick={false}
closeable={true}
onClose={closeLoginByPhone}
>
<Form
style={{width: '350px',padding: '10px'}}
divider
initialValues={FormData}
labelPosition="left"
onFinish={(values) => submitByPhone(values)}
footer={
<div
style={{
display: 'flex',
justifyContent: 'center',
width: '100%'
}}
>
<Button nativeType="submit" block style={{backgroundColor: '#000000',color: '#ffffff'}}>
提交
</Button>
</div>
}
>
<Form.Item
label={'手机号码'}
name="phone"
required
rules={[{message: '手机号码'}]}
>
<Input placeholder="请输入手机号码" maxLength={11} type="text"/>
</Form.Item>
<Form.Item
label={'短信验证码'}
name="code"
required
rules={[{message: '短信验证码'}]}
>
<Input placeholder="请输入短信验证码" maxLength={6} type="text"/>
</Form.Item>
</Form>
</Popup>
</>
)
}
export default UserFooter
-69
View File
@@ -1,69 +0,0 @@
import {useEffect} from "react";
import navTo from "@/utils/common";
import {View, Text} from '@tarojs/components';
import {ArrowRight, Wallet, Comment, Transit, Refund, Package} from '@nutui/icons-react-taro';
function UserOrder() {
const reload = () => {
};
useEffect(() => {
reload()
}, []);
return (
<>
<View className={'px-4 pb-2'}>
<View
className={'user-card w-full flex flex-col justify-around rounded-xl shadow-sm'}
style={{
background: 'linear-gradient(to bottom, #ffffff, #ffffff)', // 这种情况建议使用类名来控制样式(引入外联样式)
// margin: '10px auto 0px auto',
height: '120px',
// borderRadius: '22px 22px 0 0',
}}
>
<View className={'title-bar flex justify-between pt-2'}>
<Text className={'title font-medium px-4'}>我的订单</Text>
<View className={'more flex items-center px-2'} onClick={() => navTo('/user/order/order', true)}>
<Text className={'text-xs text-gray-500'}>全部订单</Text>
<ArrowRight color="#cccccc" size={12}/>
</View>
</View>
<View className={'flex justify-around pb-1'}>
<View className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/order/order?statusFilter=0', true)}>
<Wallet size={26} className={'font-normal text-gray-500'}/>
<Text className={'text-sm text-gray-600 py-1'}>待付款</Text>
</View>
<View className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/order/order?statusFilter=1', true)}>
<Package size={26} className={'text-gray-500 font-normal'}/>
<Text className={'text-sm text-gray-600 py-1'}>待发货</Text>
</View>
<View className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/order/order?statusFilter=3', true)}>
<Transit size={24} className={'text-gray-500 font-normal'}/>
<Text className={'text-sm text-gray-600 py-1'}>待收货</Text>
</View>
<View className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/order/order?statusFilter=5', true)}>
<Comment size={24} className={'text-gray-500 font-normal'}/>
<Text className={'text-sm text-gray-600 py-1'}>已完成</Text>
</View>
<View className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/order/order?statusFilter=6', true)}>
<Refund size={26} className={'font-normal text-gray-500'}/>
<Text className={'text-sm text-gray-600 py-1'}>退货/售后</Text>
</View>
</View>
</View>
</View>
</>
)
}
export default UserOrder;
-3
View File
@@ -1,3 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '管理中心'
})
-49
View File
@@ -1,49 +0,0 @@
import { View, Text } from '@tarojs/components'
import Taro from '@tarojs/taro'
import { ConfigProvider, Grid } from '@nutui/nutui-react-taro'
import { useUser } from '@/hooks/useUser'
import { useThemeStyles } from '@/hooks/useTheme'
import { Edit } from '@nutui/icons-react-taro'
function Admin() {
const { isAdmin } = useUser()
const themeStyles = useThemeStyles()
if (!isAdmin()) {
return (
<View className="bg-gray-100 min-h-screen flex items-center justify-center">
<View className="text-center">
<Text className="text-gray-400 text-sm">暂无访问权限</Text>
</View>
</View>
)
}
return (
<View className="bg-gray-100 min-h-screen">
<View className="px-4 py-6" style={themeStyles.primaryBackground}>
<Text className="text-white text-lg font-bold">内容管理</Text>
<View className="mt-1">
<Text className="text-white text-sm opacity-80">管理文章、内容等数据</Text>
</View>
</View>
<View className="bg-white mx-4 mt-4 rounded-xl p-4">
<Text className="font-semibold text-gray-800 mb-4 block">管理工具</Text>
<ConfigProvider>
<Grid columns={4}>
<Grid.Item text="文章管理" onClick={() => Taro.navigateTo({ url: '/admin/article/index' })}>
<View className="text-center">
<View className="w-12 h-12 bg-blue-500 rounded-xl flex items-center justify-center mx-auto mb-2">
<Edit color="#3b82f6" size="20" />
</View>
</View>
</Grid.Item>
</Grid>
</ConfigProvider>
</View>
</View>
)
}
export default Admin
-320
View File
@@ -1,320 +0,0 @@
import { request } from '../utils/request'
// 售后类型
export type AfterSaleType = 'refund' | 'return' | 'exchange' | 'repair'
// 售后状态
export type AfterSaleStatus =
| 'pending' // 待审核
| 'approved' // 已同意
| 'rejected' // 已拒绝
| 'processing' // 处理中
| 'completed' // 已完成
| 'cancelled' // 已取消
// 售后进度记录
export interface ProgressRecord {
id: string
time: string
status: string
description: string
operator?: string
remark?: string
images?: string[]
}
// 售后详情
export interface AfterSaleDetail {
id: string
orderId: string
orderNo: string
type: AfterSaleType
status: AfterSaleStatus
reason: string
description: string
amount: number
applyTime: string
processTime?: string
completeTime?: string
rejectReason?: string
contactPhone?: string
evidenceImages: string[]
progressRecords: ProgressRecord[]
}
// 售后申请参数
export interface AfterSaleApplyParams {
orderId: string
type: AfterSaleType
reason: string
description: string
amount: number
contactPhone?: string
evidenceImages?: string[]
goodsItems?: Array<{
goodsId: string
quantity: number
}>
}
// 售后列表查询参数
export interface AfterSaleListParams {
page?: number
pageSize?: number
status?: AfterSaleStatus
type?: AfterSaleType
startTime?: string
endTime?: string
}
// 售后列表响应
export interface AfterSaleListResponse {
success: boolean
data: {
list: AfterSaleDetail[]
total: number
page: number
pageSize: number
}
message?: string
}
// 售后详情响应
export interface AfterSaleDetailResponse {
success: boolean
data: AfterSaleDetail
message?: string
}
// 售后类型映射
export const AFTER_SALE_TYPE_MAP = {
'refund': '退款',
'return': '退货',
'exchange': '换货',
'repair': '维修'
}
// 售后状态映射
export const AFTER_SALE_STATUS_MAP = {
'pending': '待审核',
'approved': '已同意',
'rejected': '已拒绝',
'processing': '处理中',
'completed': '已完成',
'cancelled': '已取消'
}
// 状态颜色映射
export const STATUS_COLOR_MAP = {
'pending': '#faad14',
'approved': '#52c41a',
'rejected': '#ff4d4f',
'processing': '#1890ff',
'completed': '#52c41a',
'cancelled': '#999'
}
// 申请售后
export const applyAfterSale = async (params: AfterSaleApplyParams): Promise<AfterSaleDetailResponse> => {
try {
const response = await request<AfterSaleDetailResponse>({
url: '/api/after-sale/apply',
method: 'POST',
data: params
})
return response
} catch (error) {
console.error('申请售后失败:', error)
throw error
}
}
// 查询售后详情
export const getAfterSaleDetail = async (params: {
orderId?: string
afterSaleId?: string
}): Promise<AfterSaleDetailResponse> => {
try {
const response = await request<AfterSaleDetailResponse>({
url: '/api/after-sale/detail',
method: 'GET',
data: params
})
return response
} catch (error) {
console.error('查询售后详情失败:', error)
// 返回模拟数据作为降级方案
return getMockAfterSaleDetail(params)
}
}
// 查询售后列表
export const getAfterSaleList = async (params: AfterSaleListParams): Promise<AfterSaleListResponse> => {
try {
const response = await request<AfterSaleListResponse>({
url: '/api/after-sale/list',
method: 'GET',
data: params
})
return response
} catch (error) {
console.error('查询售后列表失败:', error)
throw error
}
}
// 撤销售后申请
export const cancelAfterSale = async (afterSaleId: string): Promise<{ success: boolean; message?: string }> => {
try {
const response = await request<{ success: boolean; message?: string }>({
url: '/api/after-sale/cancel',
method: 'POST',
data: { afterSaleId }
})
return response
} catch (error) {
console.error('撤销售后申请失败:', error)
throw error
}
}
// 获取模拟售后详情数据
const getMockAfterSaleDetail = (params: {
orderId?: string
afterSaleId?: string
}): AfterSaleDetailResponse => {
const now = new Date()
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000)
const twoDaysAgo = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000)
const mockData: AfterSaleDetailResponse = {
success: true,
data: {
id: 'AS' + Date.now(),
orderId: params.orderId || '',
orderNo: 'ORD' + Date.now(),
type: 'refund',
status: 'processing',
reason: '商品质量问题',
description: '收到的商品有明显瑕疵,包装破损,希望申请退款处理',
amount: 9999,
applyTime: twoDaysAgo.toISOString(),
processTime: yesterday.toISOString(),
contactPhone: '138****5678',
evidenceImages: [
'https://via.placeholder.com/200x200?text=Evidence1',
'https://via.placeholder.com/200x200?text=Evidence2'
],
progressRecords: [
{
id: '1',
time: now.toISOString(),
status: '处理中',
description: '客服正在处理您的申请,请耐心等待',
operator: '客服小王',
remark: '预计1-2个工作日内完成处理'
},
{
id: '2',
time: new Date(now.getTime() - 4 * 60 * 60 * 1000).toISOString(),
status: '已审核',
description: '您的申请已通过审核,正在安排退款处理',
operator: '审核员张三'
},
{
id: '3',
time: yesterday.toISOString(),
status: '已受理',
description: '我们已收到您的申请,正在进行审核',
operator: '系统'
},
{
id: '4',
time: twoDaysAgo.toISOString(),
status: '已提交',
description: '您已成功提交售后申请',
operator: '用户'
}
]
}
}
return mockData
}
// 格式化售后状态
export const formatAfterSaleStatus = (status: AfterSaleStatus): {
text: string
color: string
icon: string
} => {
const statusMap = {
'pending': { text: '待审核', color: '#faad14', icon: '⏳' },
'approved': { text: '已同意', color: '#52c41a', icon: '✅' },
'rejected': { text: '已拒绝', color: '#ff4d4f', icon: '❌' },
'processing': { text: '处理中', color: '#1890ff', icon: '🔄' },
'completed': { text: '已完成', color: '#52c41a', icon: '✅' },
'cancelled': { text: '已取消', color: '#999', icon: '⭕' }
}
return statusMap[status] || { text: status, color: '#666', icon: '📋' }
}
// 计算预计处理时间
export const calculateEstimatedTime = (
applyTime: string,
type: AfterSaleType,
status: AfterSaleStatus
): string => {
const applyDate = new Date(applyTime)
let estimatedDays = 3 // 默认3个工作日
// 根据售后类型调整预计时间
switch (type) {
case 'refund':
estimatedDays = 3 // 退款3个工作日
break
case 'return':
estimatedDays = 7 // 退货7个工作日
break
case 'exchange':
estimatedDays = 10 // 换货10个工作日
break
case 'repair':
estimatedDays = 15 // 维修15个工作日
break
}
// 根据当前状态调整
if (status === 'completed') {
return '已完成'
} else if (status === 'rejected' || status === 'cancelled') {
return '已结束'
}
const estimatedDate = new Date(applyDate.getTime() + estimatedDays * 24 * 60 * 60 * 1000)
return `预计${estimatedDate.getMonth() + 1}月${estimatedDate.getDate()}日前完成`
}
// 获取售后进度步骤
export const getAfterSaleSteps = (type: AfterSaleType, status: AfterSaleStatus) => {
const baseSteps = [
{ title: '提交申请', description: '用户提交售后申请' },
{ title: '审核中', description: '客服审核申请材料' },
{ title: '处理中', description: '正在处理您的申请' },
{ title: '完成', description: '售后处理完成' }
]
// 根据类型调整步骤
if (type === 'return' || type === 'exchange') {
baseSteps.splice(2, 0, { title: '等待收货', description: '等待用户寄回商品' })
baseSteps.splice(3, 0, { title: '确认收货', description: '商家确认收到退回商品' })
}
return baseSteps
}
-226
View File
@@ -1,226 +0,0 @@
/**
* 数据分析/运营监控 API
*/
import { request } from '../utils/request'
import type {
OverviewStats,
ApiCallStat,
ErrorStat,
PerformanceMetric,
RegionStat,
DeviceStat,
BrowserStat,
OsStat,
SourceStat,
PageStat,
EventTrack,
AnalyticsParam,
} from '../types/analytics'
// ==================== 概览统计 ====================
/** 获取概览统计 */
export const getOverviewStats = (websiteId: number, date?: string) => {
return request<OverviewStats>({
url: '/developer/analytics/overview',
method: 'GET',
params: { websiteId, date },
})
}
/** 获取趋势数据 */
export const getTrendData = (
websiteId: number,
startDate: string,
endDate: string,
granularity: 'hour' | 'day' | 'week' | 'month' = 'day'
) => {
return request<{ list: ApiCallStat[] }>({
url: '/developer/analytics/trend',
method: 'GET',
params: { websiteId, startDate, endDate, granularity },
})
}
/** 获取今日实时数据 */
export const getRealtimeData = (websiteId: number) => {
return request<{
uv: number
pv: number
apiCalls: number
errors: number
activeUsers: number
}>({
url: '/developer/analytics/realtime',
method: 'GET',
params: { websiteId },
})
}
// ==================== API 调用统计 ====================
/** 获取 API 调用统计 */
export const pageApiCalls = (params: AnalyticsParam) => {
return request<{ list: ApiCallStat[]; total: number }>({
url: '/developer/analytics/api-calls',
method: 'GET',
params,
})
}
// ==================== 错误统计 ====================
/** 获取错误列表 */
export const pageErrors = (params: AnalyticsParam) => {
return request<{ list: ErrorStat[]; total: number }>({
url: '/developer/analytics/errors',
method: 'GET',
params,
})
}
/** 获取错误详情 */
export const getErrorDetail = (id: number) => {
return request<ErrorStat & { samples: any[] }>({
url: `/developer/analytics/errors/${id}`,
method: 'GET',
})
}
// ==================== 性能指标 ====================
/** 获取性能指标 */
export const getPerformanceMetrics = (websiteId: number) => {
return request<{ list: PerformanceMetric[] }>({
url: '/developer/analytics/performance',
method: 'GET',
params: { websiteId },
})
}
/** 获取性能趋势 */
export const getPerformanceTrend = (
websiteId: number,
metric: string,
startDate: string,
endDate: string
) => {
return request<{ list: PerformanceMetric[] }>({
url: '/developer/analytics/performance/trend',
method: 'GET',
params: { websiteId, metric, startDate, endDate },
})
}
// ==================== 用户统计 ====================
/** 获取用户统计 */
export const getUserStats = (websiteId: number) => {
return request<{
total: number
active: number
new: number
retention: number
}>({
url: '/developer/analytics/users',
method: 'GET',
params: { websiteId },
})
}
/** 获取新增用户趋势 */
export const getNewUsersTrend = (
websiteId: number,
startDate: string,
endDate: string
) => {
return request<{ list: { date: string; count: number }[] }>({
url: '/developer/analytics/users/trend',
method: 'GET',
params: { websiteId, startDate, endDate },
})
}
// ==================== 区域分布 ====================
/** 获取区域分布 */
export const getRegionStats = (websiteId: number) => {
return request<{ list: RegionStat[] }>({
url: '/developer/analytics/regions',
method: 'GET',
params: { websiteId },
})
}
// ==================== 设备统计 ====================
/** 获取设备类型统计 */
export const getDeviceStats = (websiteId: number) => {
return request<{ list: DeviceStat[] }>({
url: '/developer/analytics/devices',
method: 'GET',
params: { websiteId },
})
}
/** 获取浏览器统计 */
export const getBrowserStats = (websiteId: number) => {
return request<{ list: BrowserStat[] }>({
url: '/developer/analytics/browsers',
method: 'GET',
params: { websiteId },
})
}
/** 获取操作系统统计 */
export const getOsStats = (websiteId: number) => {
return request<{ list: OsStat[] }>({
url: '/developer/analytics/os',
method: 'GET',
params: { websiteId },
})
}
// ==================== 来源统计 ====================
/** 获取流量来源 */
export const getSourceStats = (websiteId: number) => {
return request<{ list: SourceStat[] }>({
url: '/developer/analytics/sources',
method: 'GET',
params: { websiteId },
})
}
/** 获取页面访问排行 */
export const getPageStats = (params: AnalyticsParam) => {
return request<{ list: PageStat[]; total: number }>({
url: '/developer/analytics/pages',
method: 'GET',
params,
})
}
// ==================== 事件追踪 ====================
/** 获取事件列表 */
export const pageEvents = (params: AnalyticsParam) => {
return request<{ list: EventTrack[]; total: number }>({
url: '/developer/analytics/events',
method: 'GET',
params,
})
}
/** 触发自定义事件 */
export const trackEvent = (data: {
websiteId: number
event: string
properties?: Record<string, any>
}) => {
return request<void>({
url: '/developer/analytics/track',
method: 'POST',
data,
})
}
-101
View File
@@ -1,101 +0,0 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api/index';
import type { AppCredential, AppCredentialParam } from './model';
/**
* 分页查询应用密钥凭证
*/
export async function pageAppCredential(params: AppCredentialParam) {
const res = await request.get<ApiResult<PageResult<AppCredential>>>(
'/app/app-credential/page',
params
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 查询应用密钥凭证列表
*/
export async function listAppCredential(params?: AppCredentialParam) {
const res = await request.get<ApiResult<AppCredential[]>>(
'/app/app-credential',
params
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 添加应用密钥凭证
*/
export async function addAppCredential(data: AppCredential) {
const res = await request.post<ApiResult<unknown>>(
'/app/app-credential',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 修改应用密钥凭证
*/
export async function updateAppCredential(data: AppCredential) {
const res = await request.put<ApiResult<unknown>>(
'/app/app-credential',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 删除应用密钥凭证
*/
export async function removeAppCredential(id?: number) {
const res = await request.del<ApiResult<unknown>>(
'/app/app-credential/' + id
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 批量删除应用密钥凭证
*/
export async function removeBatchAppCredential(data: (number | undefined)[]) {
const res = await request.del<ApiResult<unknown>>(
'/app/app-credential/batch',
{
data
}
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 根据id查询应用密钥凭证
*/
export async function getAppCredential(id: number) {
const res = await request.get<ApiResult<AppCredential>>(
'/app/app-credential/' + id
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
-49
View File
@@ -1,49 +0,0 @@
import type { PageParam } from '@/api/index';
/**
* 应用密钥凭证
*/
export interface AppCredential {
// 自增ID
id?: string;
// 关联应用ID
websiteId?: string;
// 凭证名称,如生产环境密钥
name?: string;
// App ID(公开)
appId?: string;
// App Secret(加密存储)
appSecret?: string;
// 凭证类型: server/client/webhook
type?: string;
// 权限范围,空格分隔
scopes?: string;
// 到期时间,NULL=永不过期
expireTime?: string;
// 最后使用时间
lastUsedAt?: string;
//
remark?: string;
// 排序(数字越小越靠前)
sortNumber?: number;
// 状态, 0正常, 1冻结
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 用户ID
userId?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 应用密钥凭证搜索条件
*/
export interface AppCredentialParam extends PageParam {
id?: number;
keywords?: string;
}
-101
View File
@@ -1,101 +0,0 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api/index';
import type { AppEvent, AppEventParam } from './model';
/**
* 分页查询应用操作动态
*/
export async function pageAppEvent(params: AppEventParam) {
const res = await request.get<ApiResult<PageResult<AppEvent>>>(
'/app/app-event/page',
params
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 查询应用操作动态列表
*/
export async function listAppEvent(params?: AppEventParam) {
const res = await request.get<ApiResult<AppEvent[]>>(
'/app/app-event',
params
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 添加应用操作动态
*/
export async function addAppEvent(data: AppEvent) {
const res = await request.post<ApiResult<unknown>>(
'/app/app-event',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 修改应用操作动态
*/
export async function updateAppEvent(data: AppEvent) {
const res = await request.put<ApiResult<unknown>>(
'/app/app-event',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 删除应用操作动态
*/
export async function removeAppEvent(id?: number) {
const res = await request.del<ApiResult<unknown>>(
'/app/app-event/' + id
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 批量删除应用操作动态
*/
export async function removeBatchAppEvent(data: (number | undefined)[]) {
const res = await request.del<ApiResult<unknown>>(
'/app/app-event/batch',
{
data
}
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 根据id查询应用操作动态
*/
export async function getAppEvent(id: number) {
const res = await request.get<ApiResult<AppEvent>>(
'/app/app-event/' + id
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
-47
View File
@@ -1,47 +0,0 @@
import type { PageParam } from '@/api/index';
/**
* 应用操作动态
*/
export interface AppEvent {
// 自增ID
id?: string;
// 关联应用ID
websiteId?: string;
// 事件类型: created/published/updated/domain_bound/member_added/status_changed
eventType?: string;
// 事件标题,如已发布
title?: string;
// 详细描述
content?: string;
// 操作人用户ID
operatorId?: string;
// 操作人名称(冗余)
operator?: string;
// 关联ID,如版本ID
refId?: string;
// 关联类型
refType?: string;
// 扩展数据
extra?: string;
// 排序(数字越小越靠前)
sortNumber?: number;
// 状态, 0正常, 1冻结
status?: number;
// 用户ID
userId?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 应用操作动态搜索条件
*/
export interface AppEventParam extends PageParam {
id?: number;
keywords?: string;
}
-19
View File
@@ -1,19 +0,0 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { AppProduct } from './model';
const BASE = '/app/product';
/** 分页查询我的应用 */
export async function pageMyApps(params?: { current?: number; size?: number }) {
const res = await request.get<ApiResult<PageResult<AppProduct>>>(BASE + '/my/page', { params });
if (res.code === 0) return res.data;
return Promise.reject(new Error(res.message));
}
/** 获取我参与的应用列表 */
export async function listJoinedApps(params?: { page?: number; limit?: number }) {
const res = await request.get<ApiResult<PageResult<AppProduct>>>(BASE + '/page', { params });
if (res.code === 0) return res.data;
return Promise.reject(new Error(res.message));
}
-51
View File
@@ -1,51 +0,0 @@
export interface AppProduct {
productId?: number
productName?: string
productCode?: string
appType?: number
icon?: string
qrcode?: string
description?: string
domain?: string
homeUrl?: string
adminUrl?: string
version?: string
status?: number
statusText?: string
running?: number
expirationTime?: string
createTime?: string
updateTime?: string
publishStatus?: string
}
export const APP_TYPE_NAME: Record<number, string> = {
10: '网站',
20: '微信小程序',
30: '抖音小程序',
40: '百度小程序',
50: '支付宝小程序',
60: 'Android APP',
70: 'iOS APP',
80: 'macOS 应用',
90: 'Windows 应用',
100: '插件',
}
export const STATUS_NAME: Record<number, string> = {
0: '未开通',
1: '运行中',
2: '维护中',
3: '已关闭',
4: '已欠费',
5: '违规停机',
}
export const STATUS_COLOR: Record<number, string> = {
0: '#6b7280',
1: '#10b981',
2: '#f59e0b',
3: '#ef4444',
4: '#ef4444',
5: '#ef4444',
}
-101
View File
@@ -1,101 +0,0 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api/index';
import type { AppUser, AppUserParam } from './model';
/**
* 分页查询应用成员
*/
export async function pageAppUser(params: AppUserParam) {
const res = await request.get<ApiResult<PageResult<AppUser>>>(
'/app/app-user/page',
params
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 查询应用成员列表
*/
export async function listAppUser(params?: AppUserParam) {
const res = await request.get<ApiResult<AppUser[]>>(
'/app/app-user',
params
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 添加应用成员
*/
export async function addAppUser(data: AppUser) {
const res = await request.post<ApiResult<unknown>>(
'/app/app-user',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 修改应用成员
*/
export async function updateAppUser(data: AppUser) {
const res = await request.put<ApiResult<unknown>>(
'/app/app-user',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 删除应用成员
*/
export async function removeAppUser(id?: number) {
const res = await request.del<ApiResult<unknown>>(
'/app/app-user/' + id
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 批量删除应用成员
*/
export async function removeBatchAppUser(data: (number | undefined)[]) {
const res = await request.del<ApiResult<unknown>>(
'/app/app-user/batch',
{
data
}
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 根据id查询应用成员
*/
export async function getAppUser(id: number) {
const res = await request.get<ApiResult<AppUser>>(
'/app/app-user/' + id
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
-41
View File
@@ -1,41 +0,0 @@
import type { PageParam } from '@/api/index';
/**
* 应用成员
*/
export interface AppUser {
// 自增ID
id?: string;
// 关联应用ID
websiteId?: string;
// 用户ID
userId?: string;
// 用户名(冗余)
username?: string;
// 头像(冗余)
avatar?: string;
// 角色: owner/admin/developer/viewer
role?: string;
// 邀请人用户ID
inviteBy?: string;
// 加入时间
inviteTime?: string;
// 排序(数字越小越靠前)
sortNumber?: number;
// 状态, 0正常, 1冻结
status?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 应用成员搜索条件
*/
export interface AppUserParam extends PageParam {
id?: number;
keywords?: string;
}
-101
View File
@@ -1,101 +0,0 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api/index';
import type { AppVersion, AppVersionParam } from './model';
/**
* 分页查询应用版本发布记录
*/
export async function pageAppVersion(params: AppVersionParam) {
const res = await request.get<ApiResult<PageResult<AppVersion>>>(
'/app/app-version/page',
params
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 查询应用版本发布记录列表
*/
export async function listAppVersion(params?: AppVersionParam) {
const res = await request.get<ApiResult<AppVersion[]>>(
'/app/app-version',
params
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 添加应用版本发布记录
*/
export async function addAppVersion(data: AppVersion) {
const res = await request.post<ApiResult<unknown>>(
'/app/app-version',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 修改应用版本发布记录
*/
export async function updateAppVersion(data: AppVersion) {
const res = await request.put<ApiResult<unknown>>(
'/app/app-version',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 删除应用版本发布记录
*/
export async function removeAppVersion(id?: number) {
const res = await request.del<ApiResult<unknown>>(
'/app/app-version/' + id
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 批量删除应用版本发布记录
*/
export async function removeBatchAppVersion(data: (number | undefined)[]) {
const res = await request.del<ApiResult<unknown>>(
'/app/app-version/batch',
{
data
}
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 根据id查询应用版本发布记录
*/
export async function getAppVersion(id: number) {
const res = await request.get<ApiResult<AppVersion>>(
'/app/app-version/' + id
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
-53
View File
@@ -1,53 +0,0 @@
import type { PageParam } from '@/api/index';
/**
* 应用版本发布记录
*/
export interface AppVersion {
// 自增ID
id?: string;
// 关联应用ID
websiteId?: string;
// 版本号,如 1.0.0
versionNo?: string;
// 版本名称
versionName?: string;
// 版本更新说明
changelog?: string;
// 安装包地址
packageUrl?: string;
// 包大小(字节)
packageSize?: string;
// 包MD5/SHA256
packageHash?: string;
// 环境: development/staging/production
env?: string;
// 状态 0=构建中 1=已发布 2=已回滚 3=构建失败
status?: number;
// 是否为当前版本
isCurrent?: string;
// 发布人用户ID
publishBy?: string;
// 发布时间
publishTime?: string;
// 备注
remark?: string;
// 排序(数字越小越靠前)
sortNumber?: number;
// 用户ID
userId?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 应用版本发布记录搜索条件
*/
export interface AppVersionParam extends PageParam {
id?: number;
keywords?: string;
}
-47
View File
@@ -1,47 +0,0 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { Notification, NotificationParam, UnreadCountResult } from './model';
const BASE = '/app/notification';
/** 分页查询通知列表 */
export async function pageNotification(params: NotificationParam) {
const res = await request.get<ApiResult<PageResult<Notification>>>(BASE + '/page', { params });
if (res.code === 0) return res.data;
return Promise.reject(new Error(res.message));
}
/** 查询最近通知 */
export async function listRecentNotification(params?: { type?: string; limit?: number }) {
const res = await request.get<ApiResult<Notification[]>>(BASE + '/recent', { params });
if (res.code === 0 && res.data) return res.data;
return Promise.reject(new Error(res.message));
}
/** 获取未读数量统计 */
export async function getUnreadCount() {
const res = await request.get<ApiResult<UnreadCountResult>>(BASE + '/unread-count');
if (res.code === 0 && res.data) return res.data;
return Promise.reject(new Error(res.message));
}
/** 标记单条通知为已读 */
export async function markNotificationRead(id: number) {
const res = await request.put<ApiResult<unknown>>(BASE + `/read/${id}`);
if (res.code === 0) return res.message;
return Promise.reject(new Error(res.message));
}
/** 标记全部已读 */
export async function markAllNotificationRead(data?: { type?: string }) {
const res = await request.put<ApiResult<unknown>>(BASE + '/read-all', data);
if (res.code === 0) return res.message;
return Promise.reject(new Error(res.message));
}
/** 删除单条通知 */
export async function removeNotification(id: number) {
const res = await request.del<ApiResult<unknown>>(BASE + '/' + id);
if (res.code === 0) return res.message;
return Promise.reject(new Error(res.message));
}
-41
View File
@@ -1,41 +0,0 @@
import type { PageParam } from '@/api/index';
/** 通知类型 */
export type NotificationType = 'ticket' | 'review' | 'system' | 'resource' | 'permission' | 'member' | 'payment';
/** 通知实体 */
export interface Notification {
id?: number;
userId?: number;
type?: NotificationType;
title?: string;
content?: string;
isRead?: number;
refId?: number;
refType?: string;
linkUrl?: string;
senderId?: number;
senderName?: string;
senderAvatar?: string;
tenantId?: number;
createTime?: string;
updateTime?: string;
}
/** 通知查询参数 */
export interface NotificationParam extends PageParam {
type?: NotificationType | '';
isRead?: number;
}
/** 未读统计 */
export interface UnreadCountResult {
total?: number;
ticket?: number;
review?: number;
system?: number;
resource?: number;
permission?: number;
member?: number;
payment?: number;
}
-54
View File
@@ -1,54 +0,0 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { Ticket, TicketReply, TicketSubmitForm, TicketQueryParams, TicketStats } from './model';
const BASE = '/app/ticket';
/** 查询我的工单(分页) */
export async function myTickets(params: TicketQueryParams) {
const res = await request.get<ApiResult<PageResult<Ticket>>>(BASE + '/my', { params });
if (res.code === 0) return res.data;
return Promise.reject(new Error(res.message));
}
/** 提交工单 */
export async function submitTicket(data: TicketSubmitForm) {
const res = await request.post<ApiResult<Ticket>>(BASE + '/submit', data);
if (res.code === 0) return res.data;
return Promise.reject(new Error(res.message));
}
/** 获取工单详情 */
export async function getTicket(ticketId: number) {
const res = await request.get<ApiResult<Ticket>>(BASE + '/' + ticketId);
if (res.code === 0 && res.data) return res.data;
return Promise.reject(new Error(res.message));
}
/** 关闭工单 */
export async function closeTicket(ticketId: number) {
const res = await request.put<ApiResult<unknown>>(BASE + `/${ticketId}/close`);
if (res.code === 0) return res.message;
return Promise.reject(new Error(res.message));
}
/** 获取工单回复列表 */
export async function getTicketReplies(ticketId: number) {
const res = await request.get<ApiResult<TicketReply[]>>(BASE + `/${ticketId}/replies`);
if (res.code === 0 && res.data) return res.data;
return Promise.reject(new Error(res.message));
}
/** 提交工单回复 */
export async function replyTicket(data: { ticketId: number; content: string }) {
const res = await request.post<ApiResult<TicketReply>>(BASE + '/reply', data);
if (res.code === 0) return res.data;
return Promise.reject(new Error(res.message));
}
/** 获取工单统计 */
export async function getTicketStats() {
const res = await request.get<ApiResult<TicketStats>>(BASE + '/stats');
if (res.code === 0 && res.data) return res.data;
return Promise.reject(new Error(res.message));
}
-64
View File
@@ -1,64 +0,0 @@
export type TicketPriority = 'low' | 'normal' | 'high' | 'urgent'
export type TicketStatus = 'pending' | 'assigned' | 'processing' | 'resolved' | 'closed' | 'rejected'
export type TicketCategory = 'bug' | 'feature' | 'consultation' | 'complaint' | 'other'
export interface Ticket {
ticketId?: number
ticketNo?: string
title?: string
content?: string
productId?: number
productName?: string
category?: TicketCategory
priority?: TicketPriority
status?: TicketStatus
attachments?: string[]
submitUserId?: number
submitUserName?: string
submitUserAvatar?: string
assigneeId?: number
assigneeName?: string
assigneeAvatar?: string
createTime?: string
updateTime?: string
resolvedTime?: string
closedTime?: string
replyCount?: number
hasUnread?: boolean
}
export interface TicketReply {
replyId?: number
ticketId?: number
content?: string
attachments?: string[]
userId?: number
userName?: string
userAvatar?: string
isStaff?: boolean
createTime?: string
}
export interface TicketSubmitForm {
title: string
content: string
productId?: number
category: TicketCategory
priority: TicketPriority
}
export interface TicketQueryParams {
status?: TicketStatus
category?: TicketCategory
keywords?: string
page?: number
limit?: number
}
export interface TicketStats {
total?: number
pending?: number
processing?: number
resolved?: number
closed?: number
}
-129
View File
@@ -1,129 +0,0 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { BszxBm, BszxBmParam } from './model';
/**
* 分页查询百色中学-报名记录
*/
export async function pageBszxBm(params: BszxBmParam) {
const res = await request.get<ApiResult<PageResult<BszxBm>>>(
'/bszx/bszx-bm/page',
{
params
}
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 查询百色中学-报名记录列表
*/
export async function listBszxBm(params?: BszxBmParam) {
const res = await request.get<ApiResult<BszxBm[]>>(
'/bszx/bszx-bm',
{
params
}
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 添加百色中学-报名记录
*/
export async function addBszxBm(data: BszxBm) {
const res = await request.post<ApiResult<BszxBm>>(
'/bszx/bszx-bm',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 修改百色中学-报名记录
*/
export async function updateBszxBm(data: BszxBm) {
const res = await request.put<ApiResult<unknown>>(
'/bszx/bszx-bm',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 删除百色中学-报名记录
*/
export async function removeBszxBm(id?: number) {
const res = await request.del<ApiResult<unknown>>(
'/bszx/bszx-bm/' + id
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 批量删除百色中学-报名记录
*/
export async function removeBatchBszxBm(data: (number | undefined)[]) {
const res = await request.del<ApiResult<unknown>>(
'/bszx/bszx-bm/batch',
{
data
}
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 根据id查询百色中学-报名记录
*/
export async function getBszxBm(id: number) {
const res = await request.get<ApiResult<BszxBm>>(
'/bszx/bszx-bm/' + id
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 查询应用-百色中学-报名记录列表
*/
export async function myPageBszxBm(params?: BszxBmParam) {
const res = await request.get<ApiResult<PageResult<BszxBm>>>(
'/bszx/bszx-bm/myPage',
params
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
export async function getPoster() {
const res = await request.get<ApiResult<BszxBm>>(
'/bszx/bszx-bm/generatePoster'
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
-70
View File
@@ -1,70 +0,0 @@
import type { PageParam } from '@/api/index';
/**
* 百色中学-报名记录
*/
export interface BszxBm {
// 自增ID
id?: number;
// 文章ID
articleId?: string;
// 类型
type?: number;
// 姓名
name?: string;
// 性别 1男 2女
sex?: string;
// 手机号码
phone?: string;
// 班级
className?: string;
// 年级
gradeName?: string;
// 居住地址
address?: string;
// 工作单位
workUnit?: string;
// 职务
position?: string;
// 是否能到场
present?: string;
// 年龄
age?: number;
// 人数
number?: number;
// 额外信息
extra?: string;
// 生成的邀请函存放路径
certificate?: string;
// 预定日期
dateTime?: string;
// 表单数据
formData?: string;
// 表单ID
formId?: number;
// 用户ID
userId?: number;
// 排序(数字越小越靠前)
sortNumber?: number;
// 备注
comments?: string;
// 状态, 0正常, 1冻结
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
}
/**
* 百色中学-报名记录搜索条件
*/
export interface BszxBmParam extends PageParam {
id?: number;
userId?: number;
orderNo?: string;
limit?: number;
keywords?: string;
}
-120
View File
@@ -1,120 +0,0 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { BszxClass, BszxClassParam } from './model';
/**
* 分页查询百色中学-班级
*/
export async function pageBszxClass(params: BszxClassParam) {
const res = await request.get<ApiResult<PageResult<BszxClass>>>(
'/bszx/bszx-class/page',
{
params
}
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 查询百色中学-班级列表
*/
export async function listBszxClass(params?: BszxClassParam) {
const res = await request.get<ApiResult<BszxClass[]>>(
'/bszx/bszx-class',
{
params
}
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 添加百色中学-班级
*/
export async function addBszxClass(data: BszxClass) {
const res = await request.post<ApiResult<unknown>>(
'/bszx/bszx-class',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 修改百色中学-班级
*/
export async function updateBszxClass(data: BszxClass) {
const res = await request.put<ApiResult<unknown>>(
'/bszx/bszx-class',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 删除百色中学-班级
*/
export async function removeBszxClass(id?: number) {
const res = await request.del<ApiResult<unknown>>(
'/bszx/bszx-class/' + id
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 批量删除百色中学-班级
*/
export async function removeBatchBszxClass(data: (number | undefined)[]) {
const res = await request.del<ApiResult<unknown>>(
'/bszx/bszx-class/batch',
{
data
}
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 根据id查询百色中学-班级
*/
export async function getBszxClass(id: number) {
const res = await request.get<ApiResult<BszxClass>>(
'/bszx/bszx-class/' + id
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 查询应用-百色中学-班级列表
*/
export async function getBszxClassForTree() {
const res = await request.get<ApiResult<BszxClass[]>>(
'/bszx/bszx-class/tree'
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
-37
View File
@@ -1,37 +0,0 @@
import type { PageParam } from '@/api/index';
/**
* 百色中学-班级
*/
export interface BszxClass {
// ID
id?: number;
// 时代ID
eraId?: number;
// 年级ID
gradeId?: number;
// 班级
name?: string;
// 班级
text?: string;
// 排序(数字越小越靠前)
sortNumber?: number;
// 备注
comments?: string;
// 状态, 0正常, 1冻结
status?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 子级
children?: BszxClass[];
}
/**
* 百色中学-班级搜索条件
*/
export interface BszxClassParam extends PageParam {
id?: number;
keywords?: string;
}
-105
View File
@@ -1,105 +0,0 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { BszxEra, BszxEraParam } from './model';
/**
* 分页查询百色中学-年代
*/
export async function pageBszxEra(params: BszxEraParam) {
const res = await request.get<ApiResult<PageResult<BszxEra>>>(
'/bszx/bszx-era/page',
{
params
}
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 查询百色中学-年代列表
*/
export async function listBszxEra(params?: BszxEraParam) {
const res = await request.get<ApiResult<BszxEra[]>>(
'/bszx/bszx-era',
{
params
}
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 添加百色中学-年代
*/
export async function addBszxEra(data: BszxEra) {
const res = await request.post<ApiResult<unknown>>(
'/bszx/bszx-era',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 修改百色中学-年代
*/
export async function updateBszxEra(data: BszxEra) {
const res = await request.put<ApiResult<unknown>>(
'/bszx/bszx-era',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 删除百色中学-年代
*/
export async function removeBszxEra(id?: number) {
const res = await request.del<ApiResult<unknown>>(
'/bszx/bszx-era/' + id
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 批量删除百色中学-年代
*/
export async function removeBatchBszxEra(data: (number | undefined)[]) {
const res = await request.del<ApiResult<unknown>>(
'/bszx/bszx-era/batch',
{
data
}
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 根据id查询百色中学-年代
*/
export async function getBszxEra(id: number) {
const res = await request.get<ApiResult<BszxEra>>(
'/bszx/bszx-era/' + id
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
-21
View File
@@ -1,21 +0,0 @@
import type { PageParam } from '@/api/index';
/**
* 百色中学-年代
*/
export interface BszxEra {
// ID
id?: number;
// 年代
name?: string;
// 租户id
tenantId?: number;
}
/**
* 百色中学-年代搜索条件
*/
export interface BszxEraParam extends PageParam {
id?: number;
keywords?: string;
}
-105
View File
@@ -1,105 +0,0 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { BszxGrade, BszxGradeParam } from './model';
/**
* 分页查询百色中学-年级
*/
export async function pageBszxGrade(params: BszxGradeParam) {
const res = await request.get<ApiResult<PageResult<BszxGrade>>>(
'/bszx/bszx-grade/page',
{
params
}
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 查询百色中学-年级列表
*/
export async function listBszxGrade(params?: BszxGradeParam) {
const res = await request.get<ApiResult<BszxGrade[]>>(
'/bszx/bszx-grade',
{
params
}
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 添加百色中学-年级
*/
export async function addBszxGrade(data: BszxGrade) {
const res = await request.post<ApiResult<unknown>>(
'/bszx/bszx-grade',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 修改百色中学-年级
*/
export async function updateBszxGrade(data: BszxGrade) {
const res = await request.put<ApiResult<unknown>>(
'/bszx/bszx-grade',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 删除百色中学-年级
*/
export async function removeBszxGrade(id?: number) {
const res = await request.del<ApiResult<unknown>>(
'/bszx/bszx-grade/' + id
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 批量删除百色中学-年级
*/
export async function removeBatchBszxGrade(data: (number | undefined)[]) {
const res = await request.del<ApiResult<unknown>>(
'/bszx/bszx-grade/batch',
{
data
}
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 根据id查询百色中学-年级
*/
export async function getBszxGrade(id: number) {
const res = await request.get<ApiResult<BszxGrade>>(
'/bszx/bszx-grade/' + id
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
-31
View File
@@ -1,31 +0,0 @@
import type { PageParam } from '@/api/index';
/**
* 百色中学-年级
*/
export interface BszxGrade {
// ID
id?: number;
// 年级
name?: string;
// 年代
eraId?: number;
// 排序(数字越小越靠前)
sortNumber?: number;
// 备注
comments?: string;
// 状态, 0正常, 1冻结
status?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
}
/**
* 百色中学-年级搜索条件
*/
export interface BszxGradeParam extends PageParam {
id?: number;
keywords?: string;
}
-138
View File
@@ -1,138 +0,0 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type {BszxPay, BszxPayParam} from "@/api/bszx/bszxPay/model";
import type {BszxBm,BszxBmParam} from "@/api/bszx/bszxBm/model";
/**
* 分页查询百色中学-捐款记录
*/
export async function pageBszxPay(params: BszxPayParam) {
const res = await request.get<ApiResult<PageResult<BszxPay>>>(
'/bszx/bszx-pay/page',
params
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 查询百色中学-捐款记录列表
*/
export async function listBszxPay(params?: BszxPayParam) {
const res = await request.get<ApiResult<BszxPay[]>>(
'/bszx/bszx-pay',
{
params
}
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 添加百色中学-捐款记录
*/
export async function addBszxPay(data: BszxPay) {
const res = await request.post<ApiResult<BszxPay>>(
'/bszx/bszx-pay',
data
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 修改百色中学-捐款记录
*/
export async function updateBszxPay(data: BszxPay) {
const res = await request.put<ApiResult<unknown>>(
'/bszx/bszx-pay',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 删除百色中学-捐款记录
*/
export async function removeBszxPay(id?: number) {
const res = await request.del<ApiResult<unknown>>(
'/bszx/bszx-pay/' + id
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 批量删除百色中学-捐款记录
*/
export async function removeBatchBszxPay(data: (number | undefined)[]) {
const res = await request.del<ApiResult<unknown>>(
'/bszx/bszx-pay/batch',
{
data
}
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 根据id查询百色中学-捐款记录
*/
export async function getBszxPay(id: number) {
const res = await request.get<ApiResult<BszxPay>>(
'/bszx/bszx-pay/' + id
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 查询应用-百色中学-报名记录列表
*/
export async function myPageBszxPay(params?: BszxBmParam) {
const res = await request.get<ApiResult<PageResult<BszxBm>>>(
'/bszx/bszx-pay/myPage',
params
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
export async function getPayCert(id: number) {
const res = await request.get<ApiResult<BszxBm>>(
'/bszx/bszx-pay/generatePayCert/' + id, {}
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
export async function getCount() {
const res = await request.get<ApiResult<any>>(
'/bszx/bszx-pay/getCount'
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
-80
View File
@@ -1,80 +0,0 @@
import type { PageParam } from '@/api/index';
import {CmsArticle} from "@/api/cms/cmsArticle/model";
/**
* 百色中学-捐款记录
*/
export interface BszxPay {
// ID
id?: number;
// 年龄
age?: number;
// 姓名
name?: string;
// 性别 1男 2女
sex?: string;
// 手机号码
phone?: string;
// 班级
className?: string;
// 年级
gradeName?: string;
// 居住地址
address?: string;
// 工作单位
workUnit?: string;
// 职务
position?: string;
// 数量
number?: number;
// 价格
price?: string;
// 额外信息
extra?: string;
// 预定日期
dateTime?: string;
// 捐赠证书
certificate?: string;
// 表单数据
formData?: string;
// 表单ID
formId?: number;
// 表单名称
formName?: string;
// 订单号
orderNo?: string;
// 用户ID
userId?: number;
// 头像
avatar?: string;
// 昵称
nickname?: string;
// 真实姓名
realName?: string;
// 排序(数字越小越靠前)
sortNumber?: number;
// 备注
comments?: string;
// 状态, 0正常, 1冻结
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
present?: any;
// 关联的文章
article?: CmsArticle;
}
/**
* 百色中学-捐款记录搜索条件
*/
export interface BszxPayParam extends PageParam {
id?: number;
userId?: number;
formId?: number;
orderNo?: string;
keywords?: string;
}
-193
View File
@@ -1,193 +0,0 @@
/**
* CI/CD 流水线 API
*/
import { request } from '../utils/request'
import type {
Build,
BuildParam,
BuildStatus,
Deploy,
DeployParam,
DeployStatus,
PipelineConfig,
RuntimeInstance,
} from '../types/cicd'
// ==================== 构建相关 ====================
/** 获取构建列表 */
export const pageBuild = (params: BuildParam) => {
return request<{ list: Build[]; total: number }>({
url: '/developer/build/page',
method: 'GET',
params,
})
}
/** 获取构建详情 */
export const getBuild = (id: number) => {
return request<Build>({
url: `/developer/build/${id}`,
method: 'GET',
})
}
/** 触发构建 */
export const triggerBuild = (data: { websiteId: number; branch?: string; commitId?: string }) => {
return request<Build>({
url: '/developer/build/trigger',
method: 'POST',
data,
})
}
/** 取消构建 */
export const cancelBuild = (id: number) => {
return request<void>({
url: `/developer/build/${id}/cancel`,
method: 'POST',
})
}
/** 获取构建日志 */
export const getBuildLogs = (id: number) => {
return request<{ logs: string }>({
url: `/developer/build/${id}/logs`,
method: 'GET',
})
}
/** 获取构建产物 */
export const getBuildArtifacts = (id: number) => {
return request<{ artifacts: any[] }>({
url: `/developer/build/${id}/artifacts`,
method: 'GET',
})
}
// ==================== 部署相关 ====================
/** 获取部署列表 */
export const pageDeploy = (params: DeployParam) => {
return request<{ list: Deploy[]; total: number }>({
url: '/developer/deploy/page',
method: 'GET',
params,
})
}
/** 获取部署详情 */
export const getDeploy = (id: number) => {
return request<Deploy>({
url: `/developer/deploy/${id}`,
method: 'GET',
})
}
/** 触发部署 */
export const triggerDeploy = (data: {
websiteId: number
buildId: number
env: string
}) => {
return request<Deploy>({
url: '/developer/deploy/trigger',
method: 'POST',
data,
})
}
/** 回滚部署 */
export const rollbackDeploy = (id: number) => {
return request<Deploy>({
url: `/developer/deploy/${id}/rollback`,
method: 'POST',
})
}
/** 获取部署日志 */
export const getDeployLogs = (id: number) => {
return request<{ logs: string }>({
url: `/developer/deploy/${id}/logs`,
method: 'GET',
})
}
// ==================== 流水线配置相关 ====================
/** 获取流水线配置 */
export const getPipelineConfig = (websiteId: number) => {
return request<PipelineConfig>({
url: `/developer/pipeline/config/${websiteId}`,
method: 'GET',
})
}
/** 更新流水线配置 */
export const updatePipelineConfig = (data: PipelineConfig) => {
return request<PipelineConfig>({
url: '/developer/pipeline/config',
method: 'PUT',
data,
})
}
/** 获取分支列表 */
export const getBranches = (websiteId: number) => {
return request<{ branches: string[] }>({
url: `/developer/pipeline/branches/${websiteId}`,
method: 'GET',
})
}
/** 添加环境变量 */
export const addEnvVar = (websiteId: number, data: { key: string; value: string; isSecret?: boolean }) => {
return request<void>({
url: `/developer/pipeline/env/${websiteId}`,
method: 'POST',
data,
})
}
/** 删除环境变量 */
export const deleteEnvVar = (websiteId: number, key: string) => {
return request<void>({
url: `/developer/pipeline/env/${websiteId}/${key}`,
method: 'DELETE',
})
}
// ==================== 运行时相关 ====================
/** 获取运行时实例列表 */
export const pageRuntime = (websiteId: number, env?: string) => {
return request<{ list: RuntimeInstance[]; total: number }>({
url: '/developer/runtime/page',
method: 'GET',
params: { websiteId, env },
})
}
/** 启动实例 */
export const startRuntime = (id: number) => {
return request<void>({
url: `/developer/runtime/${id}/start`,
method: 'POST',
})
}
/** 停止实例 */
export const stopRuntime = (id: number) => {
return request<void>({
url: `/developer/runtime/${id}/stop`,
method: 'POST',
})
}
/** 重启实例 */
export const restartRuntime = (id: number) => {
return request<void>({
url: `/developer/runtime/${id}/restart`,
method: 'POST',
})
}
+1
View File
@@ -55,5 +55,6 @@ export interface CmsAdParam extends PageParam {
adType?: string;
pageId?: number;
pageName?: string;
status?: number;
keywords?: string;
}
+16 -1
View File
@@ -1,6 +1,6 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type {CmsWebsiteField, CmsWebsiteFieldParam} from './model';
import type {CmsWebsiteField, CmsWebsiteFieldParam, Config} from './model';
/**
* 分页查询应用参数
@@ -112,3 +112,18 @@ export async function undeleteWebsiteField(id?: number) {
}
return Promise.reject(new Error(res.message));
}
/**
* 查询项目参数列表
*/
export async function configWebsiteField(params?: CmsWebsiteFieldParam) {
const res = await request.get<ApiResult<Config>>(
'/cms/cms-website-field/config',
params
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
-444
View File
@@ -1,444 +0,0 @@
import request from '@/utils/request'
import type { ApiResult, PageResult } from '@/api'
import type {
App,
AppParam,
ApiKey,
ApiKeyParam,
Developer,
DeveloperApplyParam,
Project,
ProjectParam,
ProjectMember,
Version,
VersionParam,
Notification,
NotificationParam,
Apply,
ApplyParam,
} from '@/types/developer'
// ==================== 项目/应用相关 ====================
// 注意:后端使用 AppProduct 作为项目/应用的概念,路径为 /app/product
/**
* 获取我的项目列表(我创建的应用)
*/
export async function pageMyProject(params?: ProjectParam) {
const res = await request.get<ApiResult<PageResult<Project>>>('/app/product/my/page', params)
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
/**
* 获取项目列表(分页查询应用列表)
*/
export async function pageProject(params?: ProjectParam) {
const res = await request.get<ApiResult<PageResult<Project>>>('/app/product/page', params)
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
/**
* 获取项目详情
*/
export async function getProject(id: number) {
const res = await request.get<ApiResult<Project>>(`/app/product/detail/${id}`)
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
/**
* 创建项目
*/
export async function createProject(data: Partial<Project>) {
const res = await request.post<ApiResult<unknown>>('/app/product/create', data)
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
/**
* 更新项目
*/
export async function updateProject(data: Partial<Project>) {
const res = await request.put<ApiResult<unknown>>('/app/product/update', data)
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
/**
* 删除项目
*/
export async function deleteProject(id: number) {
const res = await request.del<ApiResult<unknown>>(`/app/product/delete/${id}`)
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
/**
* 获取项目成员列表(使用应用用户服务)
*/
export async function listProjectMember(projectId: number) {
const res = await request.get<ApiResult<ProjectMember[]>>(`/app/app-user/page`, { appId: projectId })
if (res.code === 0 && res.data) return res.data
return Promise.reject(new Error(res.message))
}
/**
* 添加项目成员
*/
export async function addProjectMember(projectId: number, data: Partial<ProjectMember>) {
const res = await request.post<ApiResult<unknown>>(`/app/app-user`, { ...data, appId: projectId })
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
/**
* 移除项目成员
*/
export async function removeProjectMember(projectId: number, memberId: number) {
const res = await request.del<ApiResult<unknown>>(`/app/app-user/${memberId}`)
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
// ==================== 应用相关(别名,与项目共用)====================
/**
* 分页查询我的应用(创建的应用)
*/
export async function pageMyApp(params?: AppParam) {
const res = await request.get<ApiResult<PageResult<App>>>('/app/product/my/page', params)
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
/**
* 分页查询我参与的应用(通过邀请加入的应用)
*/
export async function pageJoinedApp(params?: AppParam) {
const res = await request.get<ApiResult<PageResult<App>>>('/app/product/joined/page', params)
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
/**
* 获取应用列表
*/
export async function pageApp(params?: AppParam) {
const res = await request.get<ApiResult<PageResult<App>>>('/app/product/page', params)
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
/**
* 获取应用详情
*/
export async function getApp(id: number) {
const res = await request.get<ApiResult<App>>(`/app/product/detail/${id}`)
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
/**
* 创建应用
*/
export async function createApp(data: Partial<App>) {
const res = await request.post<ApiResult<unknown>>('/app/product/create', data)
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
/**
* 更新应用
*/
export async function updateApp(data: Partial<App>) {
const res = await request.put<ApiResult<unknown>>('/app/product/update', data)
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
/**
* 删除应用
*/
export async function deleteApp(id: number) {
const res = await request.del<ApiResult<unknown>>(`/app/product/delete/${id}`)
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
// ==================== API Key 相关 ====================
// 对应后端 AppCredentialController,路径为 /app/app-credential
/**
* 获取 API Key 列表
*/
export async function pageApiKey(params?: ApiKeyParam) {
const res = await request.get<ApiResult<PageResult<ApiKey>>>('/app/app-credential/page', params)
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
/**
* 获取 API Key 列表(不分页)
*/
export async function listApiKey(params?: ApiKeyParam) {
const res = await request.get<ApiResult<ApiKey[]>>('/app/app-credential', params)
if (res.code === 0 && res.data) return res.data
return Promise.reject(new Error(res.message))
}
/**
* 创建 API Key
*/
export async function createApiKey(data: Partial<ApiKey>) {
const res = await request.post<ApiResult<unknown>>('/app/app-credential', data)
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
/**
* 更新 API Key
*/
export async function updateApiKey(data: Partial<ApiKey>) {
const res = await request.put<ApiResult<unknown>>('/app/app-credential', data)
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
/**
* 删除 API Key
*/
export async function deleteApiKey(id: number) {
const res = await request.del<ApiResult<unknown>>(`/app/app-credential/${id}`)
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
/**
* 重置 API Key Secret
*/
export async function resetApiKeySecret(id: number) {
const res = await request.post<ApiResult<unknown>>(`/app/app-credential/resetSecret/${id}`)
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
// ==================== 版本发布相关 ====================
// 对应后端 AppVersionController,路径为 /app/app-version
/**
* 获取版本列表
*/
export async function pageVersion(params?: VersionParam) {
const res = await request.get<ApiResult<PageResult<Version>>>('/app/app-version/page', params)
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
/**
* 获取版本详情
*/
export async function getVersion(id: number) {
const res = await request.get<ApiResult<Version>>(`/app/app-version/${id}`)
if (res.code === 0 && res.data) return res.data
return Promise.reject(new Error(res.message))
}
/**
* 创建版本
*/
export async function createVersion(data: Partial<Version>) {
const res = await request.post<ApiResult<unknown>>('/app/app-version', data)
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
/**
* 发布版本
*/
export async function publishVersion(id: number) {
const res = await request.post<ApiResult<unknown>>(`/app/app-version/publish/${id}`)
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
/**
* 回滚版本
*/
export async function rollbackVersion(id: number) {
const res = await request.post<ApiResult<unknown>>(`/app/app-version/rollback/${id}`)
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
// ==================== 开发者相关 ====================
// 对应后端 GitAccountController,路径为 /app/developer
/**
* 获取开发者信息(Git账号绑定状态)
*/
export async function getDeveloperInfo() {
const res = await request.get<ApiResult<Developer>>('/app/developer/git-account')
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
/**
* 申请成为开发者(绑定Git账号)
*/
export async function applyDeveloper(data: DeveloperApplyParam) {
const res = await request.post<ApiResult<unknown>>('/app/developer/git-account', data)
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
/**
* 更新开发者信息(更新Git账号)
*/
export async function updateDeveloperInfo(data: Partial<Developer>) {
const res = await request.put<ApiResult<unknown>>('/app/developer/git-account', data)
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
/**
* 获取Gitea服务器信息
*/
export async function getGiteaInfo() {
const res = await request.get<ApiResult<{ url: string; version: string; registrationEnabled: boolean }>>('/app/developer/gitea-info')
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
// ==================== 消息通知相关 ====================
// 对应后端 AppNotificationController,路径为 /app/notification
/**
* 获取通知列表
*/
export async function pageNotification(params?: NotificationParam) {
const res = await request.get<ApiResult<PageResult<Notification>>>('/app/notification/page', params)
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
/**
* 获取最近通知(铃铛下拉)
*/
export async function getRecentNotifications(type?: string, limit?: number) {
const res = await request.get<ApiResult<Notification[]>>('/app/notification/recent', { type, limit })
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
/**
* 获取未读通知数量
*/
export async function getUnreadCount() {
const res = await request.get<ApiResult<{ count: number }>>('/app/notification/unread-count')
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
/**
* 标记通知为已读
*/
export async function markAsRead(id: number) {
const res = await request.put<ApiResult<unknown>>(`/app/notification/read/${id}`)
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
/**
* 标记所有通知为已读
*/
export async function markAllAsRead(type?: string) {
const res = await request.put<ApiResult<unknown>>('/app/notification/read-all', { type })
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
/**
* 删除通知
*/
export async function deleteNotification(id: number) {
const res = await request.del<ApiResult<unknown>>(`/app/notification/${id}`)
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
/**
* 清空已读通知
*/
export async function clearReadNotifications(type?: string) {
const res = await request.del<ApiResult<unknown>>('/app/notification/clear-read', { type })
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
// ==================== 权限审批相关 ====================
// 对应后端 AppPermissionRequestController,路径为 /app/developer/permission-requests
/**
* 获取申请列表
*/
export async function pageApply(params?: ApplyParam) {
const res = await request.get<ApiResult<PageResult<Apply>>>('/app/developer/permission-requests/page', params)
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
/**
* 获取我的申请列表
*/
export async function pageMyApply(params?: ApplyParam) {
const res = await request.get<ApiResult<PageResult<Apply>>>('/app/developer/permission-requests', params)
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
/**
* 获取权限申请统计
*/
export async function getApplyStats() {
const res = await request.get<ApiResult<{ [key: string]: number }>>('/app/developer/permission-requests/stats')
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
/**
* 获取可申请的仓库列表
*/
export async function getAvailableRepos() {
const res = await request.get<ApiResult<Array<{ name: string; fullName: string }>>>('/app/developer/permission-requests/available-repos')
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
/**
* 创建申请
*/
export async function createApply(data: { repo: string; reason: string; gitUsername?: string }) {
const res = await request.post<ApiResult<unknown>>('/app/developer/permission-requests', data)
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
/**
* 审批申请-通过
*/
export async function approveApply(id: number, note?: string) {
const res = await request.put<ApiResult<unknown>>(`/app/developer/permission-requests/${id}/approve`, { note })
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
/**
* 审批申请-拒绝
*/
export async function rejectApply(id: number, reason: string) {
const res = await request.put<ApiResult<unknown>>(`/app/developer/permission-requests/${id}/reject`, { reason })
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
-148
View File
@@ -1,148 +0,0 @@
import request from '@/utils/request'
import type { ApiResult, PageResult } from '@/api'
import type { Enterprise, EnterpriseMember, EnterpriseMemberParam, Order, Bill, BillParam, App, AppParam } from '@/types/developer'
// ==================== 企业/租户相关 ====================
/**
* 获取企业信息
*/
export async function getEnterpriseInfo() {
const res = await request.get<ApiResult<Enterprise>>('/system/tenant/info')
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
/**
* 更新企业信息
*/
export async function updateEnterpriseInfo(data: Partial<Enterprise>) {
const res = await request.put<ApiResult<unknown>>('/system/tenant', data)
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
// ==================== 企业成员/用户相关 ====================
/**
* 获取企业成员列表
*/
export async function pageEnterpriseMember(params?: EnterpriseMemberParam) {
const res = await request.get<ApiResult<PageResult<EnterpriseMember>>>('/system/user/page', params)
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
/**
* 获取企业成员列表(不分页)
*/
export async function listEnterpriseMember(params?: EnterpriseMemberParam) {
const res = await request.get<ApiResult<EnterpriseMember[]>>('/system/user', params)
if (res.code === 0 && res.data) return res.data
return Promise.reject(new Error(res.message))
}
/**
* 邀请企业成员
*/
export async function inviteEnterpriseMember(enterpriseId: number, data: Partial<EnterpriseMember>) {
const res = await request.post<ApiResult<unknown>>(`/app/developer/invite/${enterpriseId}/invite`, data)
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
/**
* 更新企业成员
*/
export async function updateEnterpriseMember(data: Partial<EnterpriseMember>) {
const res = await request.put<ApiResult<unknown>>(`/system/user`, data)
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
/**
* 移除企业成员
*/
export async function removeEnterpriseMember(memberId: number) {
const res = await request.del<ApiResult<unknown>>(`/system/user/${memberId}`)
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
// ==================== 订单相关 ====================
/**
* 获取订单列表
*/
export async function pageOrder(params?: { page?: number; limit?: number; status?: number }) {
const res = await request.get<ApiResult<PageResult<Order>>>('/system/order/page', params)
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
/**
* 获取订单详情
*/
export async function getOrder(id: number) {
const res = await request.get<ApiResult<Order>>(`/system/order/${id}`)
if (res.code === 0 && res.data) return res.data
return Promise.reject(new Error(res.message))
}
/**
* 创建订单
*/
export async function createOrder(data: any) {
const res = await request.post<ApiResult<unknown>>('/system/order', data)
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
// ==================== 充值/账单相关 ====================
/**
* 获取账单列表
*/
export async function pageBill(params?: BillParam) {
const res = await request.get<ApiResult<PageResult<Bill>>>('/sys/recharge-order/page', params)
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
/**
* 获取账单概览
*/
export async function getBillOverview() {
const res = await request.get<ApiResult<{ balance: number; totalConsume: number; totalRecharge: number }>>('/sys/user-balance-log/overview')
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
// ==================== 企业应用相关 ====================
/**
* 获取企业应用列表
*/
export async function pageEnterpriseApp(params?: AppParam) {
const res = await request.get<ApiResult<PageResult<App>>>('/app/product/page', { params })
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
/**
* 获取企业应用详情
*/
export async function getEnterpriseApp(id: number) {
const res = await request.get<ApiResult<App>>(`/app/product/${id}`)
if (res.code === 0 && res.data) return res.data
return Promise.reject(new Error(res.message))
}
/**
* 购买应用
*/
export async function purchaseApp(productId: number) {
const res = await request.post<ApiResult<unknown>>('/api/system/order/createOrder', { productId })
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
-5
View File
@@ -1,5 +0,0 @@
/**
* 开发者 API 模块
*/
export * from './developer'
export * from './enterprise'
-331
View File
@@ -1,331 +0,0 @@
/**
* 数据导入/导出 API
*/
import Taro from '@tarojs/taro';
import type { ImportTask, ImportTemplate, ImportField, ImportError, ExportTask, ExportFormat, ExportType, ImportType } from '../types/import';
// 模拟数据
const mockImportTasks: ImportTask[] = [
{
id: '1',
name: '商品批量导入',
type: 'product',
status: 'completed',
total: 500,
success: 485,
failed: 15,
progress: 100,
fileName: 'products_20260412.xlsx',
fileSize: '1.2MB',
errorFile: '/downloads/import_error_1.xlsx',
creatorId: '1',
creatorName: '张三',
startTime: '2026-04-12 10:00:00',
endTime: '2026-04-12 10:05:30',
createTime: '2026-04-12 09:58:00'
},
{
id: '2',
name: '用户数据迁移',
type: 'user',
status: 'importing',
total: 2000,
success: 1200,
failed: 0,
progress: 60,
fileName: 'users_migration.csv',
fileSize: '3.5MB',
creatorId: '1',
creatorName: '张三',
startTime: '2026-04-12 14:00:00',
createTime: '2026-04-12 13:55:00'
},
{
id: '3',
name: '订单历史导入',
type: 'order',
status: 'validating',
total: 10000,
success: 0,
failed: 0,
progress: 25,
fileName: 'orders_history.xlsx',
fileSize: '8.2MB',
creatorId: '1',
creatorName: '张三',
startTime: '2026-04-12 15:00:00',
createTime: '2026-04-12 14:50:00'
}
];
const mockExportTasks: ExportTask[] = [
{
id: '1',
name: '月度订单导出',
type: 'order',
status: 'completed',
format: 'xlsx',
filters: { month: '2026-03' },
total: 5680,
fileName: 'orders_202603.xlsx',
fileSize: '2.8MB',
downloadUrl: '/downloads/orders_202603.xlsx',
expiresAt: '2026-04-19 15:00:00',
creatorId: '1',
creatorName: '张三',
createTime: '2026-04-12 10:00:00',
completeTime: '2026-04-12 10:02:30'
},
{
id: '2',
name: '用户数据导出',
type: 'user',
status: 'processing',
format: 'csv',
filters: { registeredAfter: '2026-01-01' },
total: 0,
creatorId: '1',
creatorName: '张三',
createTime: '2026-04-12 14:30:00'
}
];
// 导入模板字段定义
const mockTemplates: ImportTemplate[] = [
{
id: 't1',
name: '商品导入模板',
type: 'product',
description: '用于批量导入商品信息,支持商品名称、价格、库存等字段',
downloadUrl: '/templates/product_import.xlsx',
fields: [
{ key: 'name', name: '商品名称', type: 'string', required: true, maxLength: 100, example: 'iPhone 15 Pro' },
{ key: 'price', name: '价格', type: 'number', required: true, example: '8999.00' },
{ key: 'stock', name: '库存', type: 'number', required: true, example: '100' },
{ key: 'category', name: '分类', type: 'select', required: true, options: ['手机', '电脑', '配件', '其他'], example: '手机' },
{ key: 'description', name: '描述', type: 'string', required: false, maxLength: 500, example: '最新款苹果手机' },
{ key: 'status', name: '状态', type: 'select', required: true, options: ['上架', '下架'], example: '上架' },
{ key: 'images', name: '图片URL', type: 'string', required: false, example: 'https://example.com/img.jpg' }
],
rules: [
{ field: 'name', rule: 'required', message: '商品名称不能为空' },
{ field: 'price', rule: 'range', message: '价格必须大于0', params: { min: 0.01 } },
{ field: 'stock', rule: 'required', message: '库存不能为空' }
],
exampleUrl: '/templates/product_example.xlsx',
createTime: '2026-01-01'
},
{
id: 't2',
name: '用户导入模板',
type: 'user',
description: '用于批量导入用户信息,包含用户名、手机、邮箱等',
downloadUrl: '/templates/user_import.xlsx',
fields: [
{ key: 'username', name: '用户名', type: 'string', required: true, maxLength: 50, example: 'zhangsan' },
{ key: 'phone', name: '手机号', type: 'string', required: true, pattern: '^1[3-9]\\d{9}$', example: '13800138000' },
{ key: 'email', name: '邮箱', type: 'string', required: false, pattern: '^\\w+@\\w+\\.\\w+$', example: 'user@example.com' },
{ key: 'nickname', name: '昵称', type: 'string', required: false, maxLength: 30, example: '小张' },
{ key: 'role', name: '角色', type: 'select', required: true, options: ['user', 'admin'], example: 'user' }
],
rules: [
{ field: 'username', rule: 'unique', message: '用户名已存在' },
{ field: 'phone', rule: 'pattern', message: '手机号格式不正确' }
],
createTime: '2026-01-01'
},
{
id: 't3',
name: '订单导入模板',
type: 'order',
description: '用于导入历史订单数据',
downloadUrl: '/templates/order_import.xlsx',
fields: [
{ key: 'orderNo', name: '订单号', type: 'string', required: true, example: 'ORD202604120001' },
{ key: 'userId', name: '用户ID', type: 'string', required: true, example: '10001' },
{ key: 'amount', name: '订单金额', type: 'number', required: true, example: '299.00' },
{ key: 'status', name: '订单状态', type: 'select', required: true, options: ['pending', 'paid', 'shipped', 'completed', 'cancelled'], example: 'completed' },
{ key: 'createTime', name: '创建时间', type: 'date', required: true, example: '2026-04-01 10:30:00' }
],
rules: [],
createTime: '2026-01-01'
}
];
/**
* 获取导入任务列表
*/
export async function pageImportTask(params: {
type?: ImportType;
status?: string;
page?: number;
pageSize?: number;
}): Promise<{ list: ImportTask[]; total: number }> {
const { type, status, page = 1, pageSize = 10 } = params;
let filtered = [...mockImportTasks];
if (type) filtered = filtered.filter(t => t.type === type);
if (status) filtered = filtered.filter(t => t.status === status);
const start = (page - 1) * pageSize;
return { list: filtered.slice(start, start + pageSize), total: filtered.length };
}
/**
* 获取导入任务详情
*/
export async function getImportTaskDetail(id: string): Promise<ImportTask | null> {
return mockImportTasks.find(t => t.id === id) || null;
}
/**
* 获取导入错误详情
*/
export async function getImportErrors(id: string): Promise<ImportError[]> {
return [
{ row: 15, field: 'price', value: '-100', error: '价格不能为负数', suggestion: '请输入正数' },
{ row: 28, field: 'name', value: '', error: '商品名称不能为空', suggestion: '请填写商品名称' },
{ row: 56, field: 'stock', value: 'abc', error: '库存必须是数字', suggestion: '请输入整数' }
];
}
/**
* 获取导入模板列表
*/
export async function listImportTemplate(): Promise<ImportTemplate[]> {
return mockTemplates;
}
/**
* 获取模板字段详情
*/
export async function getTemplateFields(type: ImportType): Promise<ImportField[]> {
const template = mockTemplates.find(t => t.type === type);
return template?.fields || [];
}
/**
* 创建导入任务
*/
export async function createImportTask(data: {
name: string;
type: ImportType;
fileName: string;
fileSize: string;
}): Promise<ImportTask> {
const task: ImportTask = {
id: String(Date.now()),
name: data.name,
type: data.type,
status: 'uploading',
total: 0,
success: 0,
failed: 0,
progress: 0,
fileName: data.fileName,
fileSize: data.fileSize,
creatorId: '1',
creatorName: '张三',
startTime: new Date().toISOString().replace('T', ' ').slice(0, 19),
createTime: new Date().toISOString().replace('T', ' ').slice(0, 19)
};
mockImportTasks.unshift(task);
return task;
}
/**
* 取消导入任务
*/
export async function cancelImportTask(id: string): Promise<void> {
const task = mockImportTasks.find(t => t.id === id);
if (task) task.status = 'cancelled';
}
/**
* 删除导入任务
*/
export async function deleteImportTask(id: string): Promise<void> {
const index = mockImportTasks.findIndex(t => t.id === id);
if (index > -1) mockImportTasks.splice(index, 1);
}
/**
* 获取导出任务列表
*/
export async function pageExportTask(params: {
type?: ExportType;
status?: string;
page?: number;
pageSize?: number;
}): Promise<{ list: ExportTask[]; total: number }> {
const { type, status, page = 1, pageSize = 10 } = params;
let filtered = [...mockExportTasks];
if (type) filtered = filtered.filter(t => t.type === type);
if (status) filtered = filtered.filter(t => t.status === status);
const start = (page - 1) * pageSize;
return { list: filtered.slice(start, start + pageSize), total: filtered.length };
}
/**
* 创建导出任务
*/
export async function createExportTask(data: {
name: string;
type: ExportType;
format: ExportFormat;
filters?: Record<string, any>;
}): Promise<ExportTask> {
const task: ExportTask = {
id: String(Date.now()),
name: data.name,
type: data.type,
status: 'pending',
format: data.format,
filters: data.filters || {},
total: 0,
creatorId: '1',
creatorName: '张三',
createTime: new Date().toISOString().replace('T', ' ').slice(0, 19)
};
mockExportTasks.unshift(task);
return task;
}
/**
* 删除导出任务
*/
export async function deleteExportTask(id: string): Promise<void> {
const index = mockExportTasks.findIndex(t => t.id === id);
if (index > -1) mockExportTasks.splice(index, 1);
}
/**
* 下载导入模板
*/
export function downloadTemplate(type: ImportType): void {
const template = mockTemplates.find(t => t.type === type);
if (template) {
Taro.showToast({ title: '开始下载模板', icon: 'success' });
console.log(`Downloading: ${template.downloadUrl}`);
}
}
/**
* 导出账单
*/
export async function exportBill(params: {
startDate: string;
endDate: string;
format: ExportFormat;
}): Promise<ExportTask> {
return createExportTask({
name: `账单导出 ${params.startDate} ~ ${params.endDate}`,
type: 'bill',
format: params.format,
filters: { startDate: params.startDate, endDate: params.endDate }
});
}
-109
View File
@@ -1,109 +0,0 @@
/**
* 发票管理 API
*/
import { request } from '../utils/request'
import type {
Invoice,
InvoiceTitle,
InvoiceApplyParam,
InvoiceParam,
InvoiceStatus,
InvoiceType,
} from '../types/invoice'
// ==================== 发票抬头 ====================
/** 获取发票抬头列表 */
export const listInvoiceTitles = () => {
return request<{ list: InvoiceTitle[] }>({
url: '/enterprise/invoice/titles',
method: 'GET',
})
}
/** 创建发票抬头 */
export const createInvoiceTitle = (data: Omit<InvoiceTitle, 'id' | 'createTime'>) => {
return request<InvoiceTitle>({
url: '/enterprise/invoice/titles',
method: 'POST',
data,
})
}
/** 更新发票抬头 */
export const updateInvoiceTitle = (id: number, data: Partial<InvoiceTitle>) => {
return request<InvoiceTitle>({
url: `/enterprise/invoice/titles/${id}`,
method: 'PUT',
data,
})
}
/** 删除发票抬头 */
export const deleteInvoiceTitle = (id: number) => {
return request<void>({
url: `/enterprise/invoice/titles/${id}`,
method: 'DELETE',
})
}
/** 设置默认发票抬头 */
export const setDefaultInvoiceTitle = (id: number) => {
return request<void>({
url: `/enterprise/invoice/titles/${id}/default`,
method: 'POST',
})
}
// ==================== 发票申请 ====================
/** 获取发票列表 */
export const pageInvoice = (params: InvoiceParam) => {
return request<{ list: Invoice[]; total: number }>({
url: '/enterprise/invoice/page',
method: 'GET',
params,
})
}
/** 获取发票详情 */
export const getInvoice = (id: number) => {
return request<Invoice>({
url: `/enterprise/invoice/${id}`,
method: 'GET',
})
}
/** 申请发票 */
export const applyInvoice = (data: InvoiceApplyParam) => {
return request<Invoice>({
url: '/enterprise/invoice/apply',
method: 'POST',
data,
})
}
/** 取消发票申请 */
export const cancelInvoice = (id: number) => {
return request<void>({
url: `/enterprise/invoice/${id}/cancel`,
method: 'POST',
})
}
/** 重新申请发票 */
export const reapplyInvoice = (id: number, data: Partial<InvoiceApplyParam>) => {
return request<Invoice>({
url: `/enterprise/invoice/${id}/reapply`,
method: 'POST',
data,
})
}
/** 获取可开票金额 */
export const getInvoiceableAmount = () => {
return request<{ amount: number }>({
url: '/enterprise/invoice/amount',
method: 'GET',
})
}
-1
View File
@@ -62,7 +62,6 @@ export async function getNext7day() {
const res = await request.get<ApiResult<any>>(
'/cms/website/getNext7day'
);
console.log('res.data.code: ', res.data.code);
if (res.code === 0 && res.data) {
return res.data;
}
+10 -7
View File
@@ -7,9 +7,10 @@ import type {
SmsCaptchaResult
} from './model';
import {saveStorageByLoginUser, SERVER_API_URL} from "@/utils/server";
import { isUserDisabled } from '@/utils/auth';
/**
* 登录
* 账号密码登录
*/
export async function login(data: LoginParam) {
const res = await request.post<ApiResult<LoginResult>>(
@@ -17,13 +18,12 @@ export async function login(data: LoginParam) {
data
);
if (res.code === 0) {
// setToken(res.data.data?.access_token, data.remember);
if (res.data?.user) {
const user = res.data?.user;
localStorage.setItem('TenantId', String(user.tenantId));
localStorage.setItem('UserId', String(user.userId));
if (res.data?.user && res.data?.access_token) {
if (isUserDisabled(res.data.user)) {
return Promise.reject(new Error('您的账号已被禁用,请联系管理员'));
}
saveStorageByLoginUser(res.data.access_token, res.data.user)
}
return res.message;
}
return Promise.reject(new Error(res.message));
@@ -50,6 +50,9 @@ export async function loginBySms(data: LoginParam) {
);
if (res.code === 0) {
if(res.data?.user){
if (isUserDisabled(res.data.user)) {
return Promise.reject(new Error('您的账号已被禁用,请联系管理员'));
}
saveStorageByLoginUser(`${res.data?.access_token}`, res.data?.user)
}
return res.message;
+6
View File
@@ -15,6 +15,10 @@ export interface LoginParam {
phone?: string;
// 短信验证码
code?: string;
// 图形验证码Key
captchaKey?: string;
// 图形验证码文本
captchaText?: string;
}
/**
@@ -31,6 +35,8 @@ export interface LoginResult {
* 图形验证码返回结果
*/
export interface CaptchaResult {
// 验证码Key
key?: string;
// 图形验证码base64数据
base64: string;
// 验证码文本
+12 -42
View File
@@ -78,36 +78,19 @@ export interface ConfirmLoginParam {
deviceInfo?: string;
}
// 确认登录响应(适配后端 QrLoginStatusResponse)
// 确认登录响应
export interface ConfirmLoginResult {
// 状态: pending-等待扫码, scanned-已扫码, confirmed-已确认, bind_phone-待绑定手机号, expired-已过期
status: string;
// JWT访问令牌(仅在confirmed状态时返回)
accessToken?: string;
// 用户信息
// 是否成功
success: boolean;
// 消息
message: string;
// 登录用户信息
userInfo?: {
userId: number;
username?: string;
nickname?: string;
avatar?: string;
phone?: string;
};
// 剩余过期时间(秒)
expiresIn?: number;
// 租户ID
tenantId?: number;
// 是否需要绑定手机号
needBindPhone?: boolean;
// 状态提示信息
message?: string;
// 下一步操作:bind_phone-绑定手机号, redirect-跳转, wait-等待
nextAction?: string;
// 跳转URL(当nextAction为redirect时使用)
redirectUrl?: string;
// 成功消息
successMessage?: string;
// 兼容字段:如果后端返回 success
success?: boolean;
}
/**
@@ -122,8 +105,7 @@ export async function generateQRToken(data?: GenerateQRTokenParam) {
...data
}
);
// 兼容 code === 0 和 code === 200
if ((res.code === 0 || res.code === 200) && res.data) {
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
@@ -136,8 +118,7 @@ export async function checkQRLoginStatus(token: string) {
const res = await request.get<ApiResult<QRLoginStatusResult>>(
SERVER_API_URL + `/qr-login/status/${token}`
);
// 兼容 code === 0 和 code === 200
if ((res.code === 0 || res.code === 200) && res.data) {
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
@@ -145,22 +126,16 @@ export async function checkQRLoginStatus(token: string) {
/**
* 确认扫码登录(通用接口)
*
* @description 小程序端确认扫码登录
* @param data - 包含 token 和用户信息
*/
export async function confirmQRLogin(data: ConfirmLoginParam) {
const res = await request.post<ApiResult<ConfirmLoginResult>>(
SERVER_API_URL + '/qr-login/confirm',
data
);
console.log('[QRLogin API] confirmQRLogin 响应:', res);
// 兼容 code === 0 和 code === 200
if ((res.code === 0 || res.code === 200) && res.data) {
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message || '登录确认失败'));
return Promise.reject(new Error(res.message));
}
/**
@@ -187,13 +162,10 @@ export async function confirmWechatQRLogin(token: string, userId: number) {
SERVER_API_URL + '/qr-login/confirm',
data
);
console.log('[QRLogin API] confirmWechatQRLogin 响应:', res);
// 兼容 code === 0 和 code === 200
if ((res.code === 0 || res.code === 200) && res.data) {
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message || '确认登录失败'));
return Promise.reject(new Error(res.message));
} catch (error: any) {
return Promise.reject(new Error(error.message || '确认登录失败'));
}
@@ -229,8 +201,6 @@ async function getDeviceInfo() {
*/
export function parseQRContent(qrContent: string): string | null {
try {
console.log('解析二维码内容1:', qrContent);
// 尝试解析JSON格式
if (qrContent.startsWith('{')) {
const parsed = JSON.parse(qrContent);
-287
View File
@@ -1,287 +0,0 @@
/**
* SDK 管理 API
*/
import type { SDK, SDKVersion, SDKDownloadRecord, SDKCategory } from '../types/sdk';
// 模拟数据
const mockSDKs: SDK[] = [
{
id: '1',
name: 'JavaScript SDK',
icon: 'https://cdn.jsdelivr.net/npm/@programming-icons/cdn/javascript@1.0.0/javascript.png',
description: '适用于 Web 浏览器和 Node.js 的 JavaScript SDK',
version: '2.8.0',
language: 'JavaScript',
category: 'web',
downloadCount: 125680,
stars: 3420,
docsUrl: '/docs/sdk/javascript',
npmPackage: '@websopy/sdk-js',
downloadUrl: 'https://www.npmjs.com/package/@websopy/sdk-js',
changelog: '- 优化性能 20%\n- 新增 TypeScript 支持\n- 修复若干 bug',
dependencies: ['axios'],
supportedVersions: ['ES6+', 'Node.js 14+'],
lastUpdated: '2026-04-10'
},
{
id: '2',
name: 'TypeScript SDK',
icon: 'https://cdn.jsdelivr.net/npm/@programming-icons/cdn/typescript@3.0.3/typescript.png',
description: '完整的 TypeScript 类型支持,适用于现代 Web 开发',
version: '2.8.0',
language: 'TypeScript',
category: 'web',
downloadCount: 98650,
stars: 2890,
docsUrl: '/docs/sdk/typescript',
npmPackage: '@websopy/sdk-ts',
downloadUrl: 'https://www.npmjs.com/package/@websopy/sdk-ts',
changelog: '- 完整的类型定义\n- 更好的 IDE 支持\n- 新增装饰器支持',
dependencies: [],
supportedVersions: ['TypeScript 4.0+'],
lastUpdated: '2026-04-10'
},
{
id: '3',
name: '微信小程序 SDK',
icon: 'https://res.wx.qq.com/a/wx_fed/assets/res/NTI4MWU5.ico',
description: '专为微信小程序优化的 SDK,支持插件机制',
version: '2.7.5',
language: 'JavaScript',
category: 'mini-program',
downloadCount: 78540,
stars: 1560,
docsUrl: '/docs/sdk/wx-mini-program',
npmPackage: '@websopy/sdk-wx',
downloadUrl: 'https://www.npmjs.com/package/@websopy/sdk-wx',
changelog: '- 适配最新微信版本\n- 新增分享能力\n- 优化包体积',
dependencies: ['wechat-miniprogram'],
supportedVersions: ['微信小程序 2.0+'],
lastUpdated: '2026-04-08'
},
{
id: '4',
name: 'Flutter SDK',
icon: 'https://cdn.jsdelivr.net/gh/flutter/website@main/src/assets/images/flutter-icon.svg',
description: 'Flutter 跨平台 SDK,支持 iOS 和 Android',
version: '1.5.2',
language: 'Flutter',
category: 'mobile',
downloadCount: 45230,
stars: 980,
docsUrl: '/docs/sdk/flutter',
downloadUrl: 'https://pub.dev/packages/websopy_flutter',
changelog: '- 支持 Flutter 3.0\n- 新增状态管理\n- 优化首屏加载',
dependencies: ['http', 'shared_preferences'],
supportedVersions: ['Flutter 2.0+', 'Dart 2.12+'],
lastUpdated: '2026-04-05'
},
{
id: '5',
name: 'React Native SDK',
icon: 'https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/512px-React-icon.svg.png',
description: 'React Native 原生模块,完美的性能体验',
version: '2.4.0',
language: 'React Native',
category: 'mobile',
downloadCount: 38560,
stars: 870,
docsUrl: '/docs/sdk/react-native',
downloadUrl: 'https://www.npmjs.com/package/@websopy/sdk-rn',
changelog: '- 新增 Hooks API\n- 支持 TypeScript\n- 修复内存泄漏',
dependencies: ['react-native'],
supportedVersions: ['React Native 0.65+', 'React 17+'],
lastUpdated: '2026-04-03'
},
{
id: '6',
name: 'Java SDK',
icon: 'https://cdn.jsdelivr.net/npm/@programming-icons/cdn/java@1.0.0/java-original.svg',
description: '企业级 Java SDK,支持 Spring Boot 集成',
version: '3.2.0',
language: 'Java',
category: 'server',
downloadCount: 65420,
stars: 1230,
docsUrl: '/docs/sdk/java',
downloadUrl: '/downloads/sdk-java-3.2.0.jar',
changelog: '- 支持 Spring Boot 3.0\n- 新增响应式编程\n- 优化连接池',
dependencies: ['slf4j', 'jackson'],
supportedVersions: ['Java 8+', 'Spring Boot 2.0+'],
lastUpdated: '2026-04-01'
},
{
id: '7',
name: 'Go SDK',
icon: 'https://cdn.jsdelivr.net/npm/@programming-icons/cdn/go@1.0.0/go-original.svg',
description: '高性能 Go SDK,支持 goroutine 并发',
version: '2.5.0',
language: 'Go',
category: 'server',
downloadCount: 32180,
stars: 760,
docsUrl: '/docs/sdk/go',
downloadUrl: 'go get github.com/websopy/sdk-go@latest',
changelog: '- 支持 Go 1.18 泛型\n- 新增中间件支持\n- 优化性能',
dependencies: [],
supportedVersions: ['Go 1.16+'],
lastUpdated: '2026-03-28'
},
{
id: '8',
name: 'Python SDK',
icon: 'https://cdn.jsdelivr.net/npm/@programming-icons/cdn/python@3.10.0/python-original.svg',
description: 'Pythonic 的 Python SDK,支持异步编程',
version: '2.6.0',
language: 'Python',
category: 'server',
downloadCount: 52100,
stars: 980,
docsUrl: '/docs/sdk/python',
downloadUrl: 'pip install websopy-sdk',
changelog: '- 支持 asyncio\n- 新增类型提示\n- 兼容 Python 3.11',
dependencies: ['requests', 'typing-extensions'],
supportedVersions: ['Python 3.7+'],
lastUpdated: '2026-03-25'
},
{
id: '9',
name: 'Swift SDK',
icon: 'https://cdn.jsdelivr.net/npm/@programming-icons/cdn/swift@1.0.0/swift-original.svg',
description: '原生 iOS/macOS Swift SDK,支持 SwiftUI',
version: '1.8.0',
language: 'Swift',
category: 'mobile',
downloadCount: 24560,
stars: 560,
docsUrl: '/docs/sdk/swift',
downloadUrl: 'https://github.com/websopy/sdk-swift/releases',
changelog: '- 支持 Swift 5.7\n- 新增 Combine 支持\n- 优化包体积',
dependencies: [],
supportedVersions: ['iOS 13+', 'macOS 11+', 'Swift 5.0+'],
lastUpdated: '2026-03-20'
},
{
id: '10',
name: '.NET SDK',
icon: 'https://cdn.jsdelivr.net/npm/@programming-icons/cdn/dot-net@1.0.0/dot-net-original.svg',
description: '.NET 5/6/7 SDK,支持 Entity Framework Core',
version: '2.3.0',
language: '.NET',
category: 'server',
downloadCount: 28940,
stars: 450,
docsUrl: '/docs/sdk/dotnet',
downloadUrl: 'https://www.nuget.org/packages/WebsopySDK',
changelog: '- 支持 .NET 7\n- 新增依赖注入\n- 支持 Blazor',
dependencies: ['Microsoft.Extensions.DependencyInjection'],
supportedVersions: ['.NET 5+', '.NET Core 3.1+'],
lastUpdated: '2026-03-18'
}
];
/**
* 获取 SDK 分类列表
*/
export async function listSDKCategories(): Promise<SDKCategory[]> {
return [
{ id: 'web', name: 'Web 开发', icon: 'globe', count: 2, description: '适用于浏览器和 Node.js 环境' },
{ id: 'mobile', name: '移动开发', icon: 'mobile', count: 3, description: 'iOS、Android、Flutter、React Native' },
{ id: 'server', name: '服务端', icon: 'server', count: 4, description: 'Java、Go、Python、.NET 等后端语言' },
{ id: 'mini-program', name: '小程序', icon: 'wechat', count: 1, description: '微信小程序、支付宝小程序等' }
];
}
/**
* 获取 SDK 列表
*/
export async function pageSDK(params: {
category?: string;
language?: string;
keyword?: string;
page?: number;
pageSize?: number;
}): Promise<{ list: SDK[]; total: number }> {
const { category, language, keyword, page = 1, pageSize = 10 } = params;
let filtered = [...mockSDKs];
if (category) {
filtered = filtered.filter(sdk => sdk.category === category);
}
if (language) {
filtered = filtered.filter(sdk => sdk.language === language);
}
if (keyword) {
const kw = keyword.toLowerCase();
filtered = filtered.filter(sdk =>
sdk.name.toLowerCase().includes(kw) ||
sdk.description.toLowerCase().includes(kw)
);
}
const start = (page - 1) * pageSize;
const list = filtered.slice(start, start + pageSize);
return { list, total: filtered.length };
}
/**
* 获取 SDK 详情
*/
export async function getSDKDetail(id: string): Promise<SDK | null> {
return mockSDKs.find(sdk => sdk.id === id) || null;
}
/**
* 获取 SDK 版本列表
*/
export async function listSDKVersions(id: string): Promise<SDKVersion[]> {
const sdk = mockSDKs.find(s => s.id === id);
if (!sdk) return [];
return [
{ version: sdk.version, releaseDate: sdk.lastUpdated, releaseNotes: sdk.changelog, downloadUrl: sdk.downloadUrl, size: '2.5MB', sha256: 'a1b2c3d4e5f6...' },
{ version: '2.7.0', releaseDate: '2026-03-15', releaseNotes: '- 性能优化\n- Bug 修复', downloadUrl: '', size: '2.3MB', sha256: 'b2c3d4e5f6g7...' },
{ version: '2.6.0', releaseDate: '2026-02-20', releaseNotes: '- 新增功能\n- 文档更新', downloadUrl: '', size: '2.1MB', sha256: 'c3d4e5f6g7h8...' }
];
}
/**
* 获取下载记录
*/
export async function pageDownloadRecord(params: {
page?: number;
pageSize?: number;
}): Promise<{ list: SDKDownloadRecord[]; total: number }> {
return {
list: [
{ id: '1', sdkId: '1', sdkName: 'JavaScript SDK', version: '2.8.0', language: 'JavaScript', userId: '1', userName: '张三', downloadTime: '2026-04-12 14:30:00', ipAddress: '127.0.0.1' },
{ id: '2', sdkId: '3', sdkName: '微信小程序 SDK', version: '2.7.5', language: 'JavaScript', userId: '1', userName: '张三', downloadTime: '2026-04-11 10:20:00', ipAddress: '127.0.0.1' },
{ id: '3', sdkId: '6', sdkName: 'Java SDK', version: '3.2.0', language: 'Java', userId: '1', userName: '张三', downloadTime: '2026-04-10 16:45:00', ipAddress: '127.0.0.1' }
],
total: 3
};
}
/**
* 记录 SDK 下载
*/
export async function recordDownload(id: string, version: string): Promise<void> {
console.log(`Download recorded: ${id} v${version}`);
}
/**
* 获取 SDK 统计
*/
export async function getSDKStats(): Promise<{
totalDownloads: number;
totalSDKs: number;
topSDKs: SDK[];
}> {
return {
totalDownloads: mockSDKs.reduce((sum, sdk) => sum + sdk.downloadCount, 0),
totalSDKs: mockSDKs.length,
topSDKs: [...mockSDKs].sort((a, b) => b.downloadCount - a.downloadCount).slice(0, 5)
};
}
+23
View File
@@ -0,0 +1,23 @@
import { generateMiniProgramCode } from '@/api/shop/shopInvite'
/**
* 生成「分享海报」用的小程序码(二维码)图片地址。
*
* - scene 复用现有 uid_${inviterId} 约定,与邀请裂变逻辑一致;
* - page 指向被分享的页面(小程序码扫码后落地页)。
*
* 注意:当前后端 /wx-login/getOrderQRCodeUnlimited/{scene} 仅消费 scene,
* 若希望扫码后直达指定页面而非默认页,需后端读取并转发 page 参数。
*
* @param page 被分享页路径,如 pages/shop/product-detail
* @param inviterId 当前登录用户 id(用于裂变归因),未登录可省略
*/
export function generateShareCode(page: string, inviterId?: number | string) {
const scene = inviterId ? `uid_${inviterId}` : 'share'
return generateMiniProgramCode({
page,
scene,
width: 200,
envVersion: 'release',
})
}
+110
View File
@@ -0,0 +1,110 @@
import request from '@/utils/request'
import type { ApiResult, PageResult } from '@/api'
import type {
ShopActivity,
ShopActivityParam,
ActivitySignUpParams,
ActivityStats,
} from './model'
import { normalizeActivity } from './model'
export type { ShopActivity, ActivityStatus, ActivityType } from './model'
/**
* 分页查询活动
*/
export async function pageShopActivity(params: ShopActivityParam) {
const res = await request.get<ApiResult<PageResult<ShopActivity>>>(
'/shop/shop-activity/page',
params
)
if (res.code === 0 && res.data) {
return {
...res.data,
list: (res.data.list || []).map(normalizeActivity),
}
}
return Promise.reject(new Error(res.message))
}
/**
* 查询活动列表
*/
export async function listShopActivity(params?: ShopActivityParam) {
const res = await request.get<ApiResult<ShopActivity[]>>(
'/shop/shop-activity',
params
)
if (res.code === 0 && res.data) {
return res.data.map(normalizeActivity)
}
return Promise.reject(new Error(res.message))
}
/**
* 根据ID查询活动
*/
export async function getShopActivity(id: number) {
const res = await request.get<ApiResult<ShopActivity>>(
'/shop/shop-activity/' + id
)
if (res.code === 0 && res.data) {
return normalizeActivity(res.data)
}
return Promise.reject(new Error(res.message))
}
/**
* 报名活动
*/
export async function signUpActivity(params: ActivitySignUpParams) {
const res = await request.post<ApiResult<unknown>>(
'/shop/shop-activity/sign-up',
params
)
if (res.code === 0) {
return res.message
}
return Promise.reject(new Error(res.message))
}
/**
* 取消报名
*/
export async function cancelSignUpActivity(activityId: number) {
const res = await request.post<ApiResult<unknown>>(
'/shop/shop-activity/cancel-sign-up',
{ activityId }
)
if (res.code === 0) {
return res.message
}
return Promise.reject(new Error(res.message))
}
/**
* 获取活动报名状态
*/
export async function getSignUpStatus(activityId: number) {
const res = await request.get<ApiResult<{ signedUp: boolean }>>(
'/shop/shop-activity/sign-up-status',
{ activityId }
)
if (res.code === 0 && res.data) {
return res.data
}
return Promise.reject(new Error(res.message))
}
/**
* 获取活动统计
*/
export async function getActivityStats() {
const res = await request.get<ApiResult<ActivityStats>>(
'/shop/shop-activity/stats'
)
if (res.code === 0 && res.data) {
return res.data
}
return Promise.reject(new Error(res.message))
}
+89
View File
@@ -0,0 +1,89 @@
/**
* 活动模型
*/
// 活动类型(前端语义化)
export type ActivityType = 'discount' | 'full_reduction' | 'seckill' | 'group_buy' | 'flash_sale'
// 活动状态(前端语义化)
export type ActivityStatus = 'upcoming' | 'ongoing' | 'ended' | 'cancelled'
// 活动数据(与后端字段对齐)
export interface ShopActivity {
id: number
name: string
image: string
banner?: string
startTime: string
endTime: string
/** 后端返回整数: 0=未开始 1=进行中 2=已结束,前端通过 normalizeActivity 转为语义化字符串 */
status: ActivityStatus | number
/** 后端返回整数: 1=满减 2=折扣 3=秒杀 4=拼团 */
type: ActivityType | number
typeName?: string
description: string
participants?: number
maxParticipants?: number
discountRate?: number
reductionAmount?: number
minAmount?: number
rules?: string
tenantId?: number
createTime: string
updateTime?: string
}
// 活动查询参数
export interface ShopActivityParam {
page?: number
limit?: number
status?: number | ActivityStatus
type?: number | ActivityType
keywords?: string
}
// 活动报名参数
export interface ActivitySignUpParams {
activityId: number
userId?: number
contactPhone?: string
remark?: string
}
// 活动统计
export interface ActivityStats {
total: number
ongoing: number
upcoming: number
ended: number
totalParticipants: number
}
// 后端整数状态 → 前端语义化状态
export function normalizeActivityStatus(status: number | ActivityStatus): ActivityStatus {
if (typeof status === 'string') return status
const map: Record<number, ActivityStatus> = { 0: 'upcoming', 1: 'ongoing', 2: 'ended' }
return map[status] ?? 'ended'
}
// 后端整数类型 → 前端语义化类型
export function normalizeActivityType(type: number | ActivityType): ActivityType {
if (typeof type === 'string') return type
const map: Record<number, ActivityType> = {
1: 'full_reduction',
2: 'discount',
3: 'seckill',
4: 'group_buy',
}
return map[type] ?? 'discount'
}
// 规范化活动对象(处理后端整数字段)
export function normalizeActivity(a: ShopActivity): ShopActivity & { status: ActivityStatus; type: ActivityType } {
return {
...a,
participants: a.participants ?? 0,
status: normalizeActivityStatus(a.status as number),
type: normalizeActivityType(a.type as number),
}
}
+154
View File
@@ -0,0 +1,154 @@
import request from '@/utils/request'
import type { ApiResult, PageResult } from '@/api'
// 售后类型
export type AfterSaleType = 'refund' | 'return' | 'exchange' | 'repair'
// 售后状态
export type AfterSaleStatus =
| 'pending' // 待审核
| 'approved' // 已同意
| 'rejected' // 已拒绝
| 'processing' // 处理中
| 'completed' // 已完成
| 'cancelled' // 已取消
// 售后进度记录
export interface ProgressRecord {
id: string
time: string
status: string
description: string
operator?: string
remark?: string
}
// 售后详情
export interface AfterSaleDetail {
id: string
orderId: string
orderNo: string
goodsName?: string
type: AfterSaleType
status: AfterSaleStatus
reason: string
description: string
amount: number
applyTime: string
processTime?: string
completeTime?: string
rejectReason?: string
contactPhone?: string
evidenceImages: string[]
progressRecords: ProgressRecord[]
}
// 售后申请参数
export interface AfterSaleApplyParams {
orderId: string
type: AfterSaleType
reason: string
description?: string
amount?: number
contactPhone?: string
evidenceImages?: string[]
goodsItems?: Array<{ goodsId: string; quantity: number }>
}
// 售后列表查询参数
export interface AfterSaleListParams {
page?: number
pageSize?: number
status?: AfterSaleStatus
type?: AfterSaleType
}
// 售后类型映射
export const AFTER_SALE_TYPE_MAP: Record<AfterSaleType, string> = {
refund: '退款',
return: '退货',
exchange: '换货',
repair: '维修',
}
// 售后状态映射
export const AFTER_SALE_STATUS_MAP: Record<AfterSaleStatus, string> = {
pending: '待审核',
approved: '已同意',
rejected: '已拒绝',
processing: '处理中',
completed: '已完成',
cancelled: '已取消',
}
// 格式化售后状态
export const formatAfterSaleStatus = (status: AfterSaleStatus): {
text: string
color: string
icon: string
} => {
const statusMap: Record<AfterSaleStatus, { text: string; color: string; icon: string }> = {
pending: { text: '待审核', color: 'text-orange-500', icon: '⏳' },
approved: { text: '已同意', color: 'text-green-500', icon: '✅' },
rejected: { text: '已拒绝', color: 'text-red-500', icon: '❌' },
processing: { text: '处理中', color: 'text-blue-500', icon: '🔄' },
completed: { text: '已完成', color: 'text-green-500', icon: '✅' },
cancelled: { text: '已取消', color: 'text-gray-400', icon: '⭕' },
}
return statusMap[status] || { text: status, color: 'text-gray-400', icon: '📋' }
}
/**
* 申请售后
*/
export async function applyAfterSale(data: AfterSaleApplyParams) {
const res = await request.post<ApiResult<AfterSaleDetail>>(
'/shop/shop-after-sale/apply',
data
)
if (res.code === 0) {
return res.data
}
return Promise.reject(new Error(res.message))
}
/**
* 分页查询售后列表
*/
export async function pageAfterSaleList(params: AfterSaleListParams) {
const res = await request.get<ApiResult<PageResult<AfterSaleDetail>>>(
'/shop/shop-after-sale/page',
params
)
if (res.code === 0) {
return res.data
}
return Promise.reject(new Error(res.message))
}
/**
* 查询售后详情
*/
export async function getAfterSaleDetail(id: string) {
const res = await request.get<ApiResult<AfterSaleDetail>>(
'/shop/shop-after-sale/' + id
)
if (res.code === 0 && res.data) {
return res.data
}
return Promise.reject(new Error(res.message))
}
/**
* 撤销售后申请
*/
export async function cancelAfterSale(id: string) {
const res = await request.post<ApiResult<unknown>>(
'/shop/shop-after-sale/cancel',
{ id }
)
if (res.code === 0) {
return res.message
}
return Promise.reject(new Error(res.message))
}
+121
View File
@@ -0,0 +1,121 @@
import request from '@/utils/request'
import type { ApiResult, PageResult } from '@/api'
import type {
ShopBooking,
ShopBookingParam,
ShopBookingParams,
BookingStats,
} from './model'
/**
* 分页查询预约订单
*/
export async function pageShopBooking(params: ShopBookingParam) {
const res = await request.get<ApiResult<PageResult<ShopBooking>>>(
'/shop/shop-booking/page',
params
)
if (res.code === 0) {
return res.data
}
return Promise.reject(new Error(res.message))
}
/**
* 查询预约订单列表
*/
export async function listShopBooking(params?: ShopBookingParam) {
const res = await request.get<ApiResult<ShopBooking[]>>(
'/shop/shop-booking',
params
)
if (res.code === 0 && res.data) {
return res.data
}
return Promise.reject(new Error(res.message))
}
/**
* 根据ID查询预约订单
*/
export async function getShopBooking(id: string) {
const res = await request.get<ApiResult<ShopBooking>>(
'/shop/shop-booking/' + id
)
if (res.code === 0 && res.data) {
return res.data
}
return Promise.reject(new Error(res.message))
}
/**
* 创建预约订单
*/
export async function createShopBooking(data: ShopBookingParams) {
const res = await request.post<ApiResult<unknown>>(
'/shop/shop-booking',
data
)
if (res.code === 0) {
return res.message
}
return Promise.reject(new Error(res.message))
}
/**
* 修改预约订单
*/
export async function updateShopBooking(data: ShopBookingParams) {
const res = await request.put<ApiResult<unknown>>(
'/shop/shop-booking',
data
)
if (res.code === 0) {
return res.message
}
return Promise.reject(new Error(res.message))
}
/**
* 取消预约订单
*/
export async function cancelShopBooking(id?: string) {
const res = await request.del<ApiResult<unknown>>(
'/shop/shop-booking/' + id
)
if (res.code === 0) {
return res.message
}
return Promise.reject(new Error(res.message))
}
/**
* 改签预约订单
*/
export async function rescheduleShopBooking(data: {
bookingId: string
newDate: string
newTime: string
}) {
const res = await request.post<ApiResult<unknown>>(
'/shop/shop-booking/reschedule',
data
)
if (res.code === 0) {
return res.message
}
return Promise.reject(new Error(res.message))
}
/**
* 获取预约统计
*/
export async function getBookingStats() {
const res = await request.get<ApiResult<BookingStats>>(
'/shop/shop-booking/stats'
)
if (res.code === 0 && res.data) {
return res.data
}
return Promise.reject(new Error(res.message))
}
+53
View File
@@ -0,0 +1,53 @@
/**
* 预约订单模型
*/
// 预约状态
export type BookingStatus = 'pending' | 'confirmed' | 'in_progress' | 'completed' | 'cancelled' | 'rescheduled'
// 预约订单数据
export interface ShopBooking {
id: number | string
bookingNo: string // 预约编号
storeId: number // 店铺ID
storeName: string // 店铺名称
storePhone?: string // 门店服务电话
serviceId: number // 服务ID
serviceName: string // 服务名称
bookingDate: string // 预约日期
bookingTime: string // 预约时间段
status: BookingStatus // 预约状态
price: number // 价格
createTime: string // 创建时间
updateTime?: string // 更新时间
remark?: string // 备注
contactName?: string // 联系人
contactPhone?: string // 联系电话
address?: string // 地址
}
// 预约查询参数
export interface ShopBookingParam {
page?: number
limit?: number
status?: BookingStatus // 按状态筛选
storeId?: number // 按店铺筛选
startDate?: string // 预约日期范围-起始
endDate?: string // 预约日期范围-结束
keywords?: string // 关键词搜索
sortBy?: string // 排序方式(createTime, bookingDate)
}
// 创建/更新预约参数
export interface ShopBookingParams extends ShopBooking {}
// 预约统计
export interface BookingStats {
total: number // 总预约数
pending: number // 待服务
confirmed: number // 已确认
inProgress: number // 进行中
completed: number // 已完成
cancelled: number // 已取消
rescheduled: number // 已改签
}
+153
View File
@@ -0,0 +1,153 @@
import request from '@/utils/request';
import type { ApiResult } from '@/api';
import type { ShopCart, ShopCartParam, AddToCartParam, UpdateCartNumParam } from './model';
/**
* 获取购物车列表
*/
export async function listShopCart(params?: ShopCartParam) {
const res = await request.get<ApiResult<ShopCart[]>>(
'/shop/shop-cart',
params
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 获取购物车分页列表
*/
export async function pageShopCart(params: ShopCartParam) {
const res = await request.get<ApiResult<{ list: ShopCart[]; count: number }>>(
'/shop/shop-cart/page',
params
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 获取购物车数量
*/
export async function getShopCartCount() {
const res = await request.get<ApiResult<{ count: number }>>(
'/shop/shop-cart/count'
);
if (res.code === 0 && res.data) {
return res.data.count;
}
return Promise.reject(new Error(res.message));
}
/**
* 添加商品到购物车
*/
export async function addToCart(data: AddToCartParam) {
const res = await request.post<ApiResult<unknown>>(
'/shop/shop-cart',
data
);
if (res.code === 0) {
return res.message || '添加成功';
}
return Promise.reject(new Error(res.message));
}
/**
* 更新购物车商品数量
*/
export async function updateCartNum(data: UpdateCartNumParam) {
const res = await request.put<ApiResult<unknown>>(
'/shop/shop-cart',
{ id: data.id, cartNum: data.cartNum }
);
if (res.code === 0) {
return res.message || '更新成功';
}
return Promise.reject(new Error(res.message));
}
/**
* 更新购物车商品选中状态
*/
export async function updateCartChecked(id: number, checked: boolean) {
const res = await request.put<ApiResult<unknown>>(
'/shop/shop-cart',
{ id, selected: checked }
);
if (res.code === 0) {
return res.message || '更新成功';
}
return Promise.reject(new Error(res.message));
}
/**
* 全选/取消全选购物车
*/
export async function updateCartAllChecked(checked: boolean) {
const res = await request.post<ApiResult<unknown>>(
'/shop/shop-cart/selected',
{ checked }
);
if (res.code === 0) {
return res.message || '更新成功';
}
return Promise.reject(new Error(res.message));
}
/**
* 删除购物车商品
*/
export async function removeShopCart(id: number) {
const res = await request.del<ApiResult<unknown>>(
'/shop/shop-cart/' + id
);
if (res.code === 0) {
return res.message || '删除成功';
}
return Promise.reject(new Error(res.message));
}
/**
* 批量删除购物车商品
*/
export async function removeBatchShopCart(ids: number[]) {
const res = await request.del<ApiResult<unknown>>(
'/shop/shop-cart/batch',
ids
);
if (res.code === 0) {
return res.message || '删除成功';
}
return Promise.reject(new Error(res.message));
}
/**
* 清空购物车
*/
export async function clearShopCart() {
const res = await request.del<ApiResult<unknown>>(
'/shop/shop-cart/clear'
);
if (res.code === 0) {
return res.message || '清空成功';
}
return Promise.reject(new Error(res.message));
}
/**
* 获取购物车详情
*/
export async function getShopCart(id: number) {
const res = await request.get<ApiResult<ShopCart>>(
'/shop/shop-cart/' + id
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
+80
View File
@@ -0,0 +1,80 @@
import type { PageParam } from '@/api';
/**
* 购物车项
*/
export interface ShopCart {
// 主键ID
id?: number;
// 用户ID
userId?: number;
// 商品ID
goodsId?: number;
// SKU ID
skuId?: number;
// 数量(后端字段名 cartNum)
cartNum?: number;
// 商品名称
goodsName?: string;
// 商品图片
goodsImage?: string;
// SKU价格
skuPrice?: string;
// SKU规格
skuSpec?: string;
// 库存
stock?: number;
// 市场价(划线价)
salePrice?: string | number;
// 经销商价(VIP专享)
dealerPrice?: string | number;
// 是否选中(后端字段名 selected)
selected?: boolean;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 更新时间
updateTime?: string;
}
/**
* 购物车项扩展(包含选中状态,用于前端展示)
*/
export interface ShopCartItem extends ShopCart {
// 是否选中
checked?: boolean;
// 商品详情
product?: any;
// SKU详情
sku?: any;
}
/**
* 添加到购物车参数
*/
export interface AddToCartParam {
// 商品ID
goodsId: number;
// SKU ID(单规格可不传)
skuId?: number;
// 数量(后端字段名 cartNum)
cartNum: number;
}
/**
* 更新购物车数量参数
*/
export interface UpdateCartNumParam {
// 购物车ID
id: number;
// 数量(后端字段名 cartNum)
cartNum: number;
}
/**
* 购物车搜索条件
*/
export interface ShopCartParam extends PageParam {
userId?: number;
}
@@ -6,6 +6,8 @@ import type { PageParam } from '@/api';
export interface ShopChatMessage {
// 自增ID
id?: number;
// 会话ID
conversationId?: number;
// 发送人ID
formUserId?: number;
// 发送人名称
@@ -60,4 +62,5 @@ export interface ShopChatMessage {
export interface ShopChatMessageParam extends PageParam {
id?: number;
keywords?: string;
conversationId?: number;
}
+29
View File
@@ -0,0 +1,29 @@
import request from '@/utils/request';
/**
* 获取我的佣金记录
*/
export function getMyCommissionList(params: any) {
return request.get('/shop/shop-commission-record/my/list', params);
}
/**
* 获取佣金统计
*/
export function getCommissionStats() {
return request.get('/shop/shop-commission-record/my/stats');
}
/**
* 获取佣金记录列表(管理员)
*/
export function getCommissionList(params: any) {
return request.get('/shop/shop-commission-record/page', params);
}
/**
* 手动触发佣金结算(管理员)
*/
export function settleCommission() {
return request.post('/shop/shop-commission-record/settle');
}
+15 -1
View File
@@ -1,6 +1,6 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { ShopCoupon, ShopCouponParam } from './model';
import type { ShopCoupon, ShopCouponParam, ShopCouponWithTake } from './model';
/**
* 分页查询优惠券
@@ -99,3 +99,17 @@ export async function getShopCoupon(id: number) {
}
return Promise.reject(new Error(res.message));
}
/**
* 领券中心-查询可领取优惠券列表(含是否已领取状态)
*/
export async function listCouponCenter(params?: ShopCouponParam) {
const res = await request.post<ApiResult<ShopCouponWithTake[]>>(
'/shop/shop-coupon/list',
params
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
+25 -1
View File
@@ -10,7 +10,7 @@ export interface ShopCoupon {
name?: string;
// 优惠券描述
description?: string;
// 优惠券类型(10满减券 20折扣券 30免费劵)
// 优惠券类型(10满减券 20折扣券 30免费券 40无门槛券 50场地使用券)
type?: number;
// 满减券-减免金额
reducePrice?: string;
@@ -54,6 +54,18 @@ export interface ShopCoupon {
limitPerUser?: number;
// 是否启用(0禁用 1启用)
enabled?: string;
// 发放对象(0全部用户 1仅会员 2仅非会员 3指定用户)
receiveTarget?: number;
// 指定用户ID列表(JSON数组格式),receiveTarget=3时使用
receiveUserIds?: string;
// 场地使用券-场地类型
venueType?: number;
// 场地使用券-指定场地ID
venueId?: number;
// 场地使用券-可用次数(-1表示无限制)
useCount?: number;
// 场地使用券-使用时长(分钟)
useDuration?: number;
sortBy?: string;
}
@@ -71,3 +83,15 @@ export interface ShopCouponParam extends PageParam {
type?: number;
minAmount?: number;
}
/**
* 领券中心优惠券(含是否已领取)
*/
export interface ShopCouponWithTake extends ShopCoupon {
// 是否已领取
hasTake?: boolean;
// 用户已领取数量
userTakeNum?: number;
// 用户已使用数量
userUseNum?: number;
}

Some files were not shown because too many files have changed in this diff Show More