diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..101974b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,88 @@ +# AGENTS.md + +森睿智传(senmesh-zc-legacy)——内网/外网文件传输与网盘管理 Web 端(React SPA)。后端为 s-file(alita-bjy-uygur 仓库),本项目为其前端。 + +## 常用命令 + +```bash +bun install # 安装依赖(使用 bun.lock;Node >= 20,本机经 nvm 提供) +bun run dev # 启动开发服务器(Vite) +bun run build # tsc -b && vite build(类型检查 + 构建,构建目标 chrome86) +bun run build:inside # 内网包(VITE_IS_INSIDE=true)→ dist/inside +bun run build:outside # 外网包(VITE_IS_INSIDE=false)→ dist/outside +bun run lint # ESLint(存在大量存量告警,见「代码约定」) +bun run preview # 预览构建产物 +``` + +内外网发布包构建后打 tar(解压后 index.html 在顶层,可直接作为 nginx 站点根): + +```bash +tar -czf release/senmesh-zc-inside.tar.gz --exclude='.DS_Store' -C dist/inside . +tar -czf release/senmesh-zc-outside.tar.gz --exclude='.DS_Store' -C dist/outside . +``` + +没有测试框架,验证手段为 `build`(含 tsc 严格检查)+ 浏览器冒烟。 + +## 关键注意事项 + +- **路由树是生成的**:`src/routeTree.gen.ts` 由 `@tanstack/router-plugin` 在 dev/build 时生成。新增路由文件后直接跑 `tsc -b` 会报路由不存在 —— 先跑 `bunx vite build`(或启动 dev)再跑 `bunx tsc -b`。 +- **Hash 路由**:`createHashHistory`,页面地址形如 `/#/admin/user`,直接访问不带 `#` 的路径会落到首页。 +- **ESLint 基线**:存量代码大量 `no-explicit-any`、`useColumns` 在回调中调用等告警属既有风格;新代码与周围风格保持一致即可,`tsc` 严格检查(`strict`、`noUnusedLocals`、`noUnusedParameters`)才是硬门禁。 + +## 技术栈 + +React 19 · TypeScript(strict) · Vite(rolldown-vite) · TanStack Router(文件路由 + autoCodeSplitting)· TanStack Query v5(全局 `retry: false`、`refetchOnWindowFocus: false`,见 `src/main.tsx`)· TanStack Table · zustand(persist + derive-zustand)· axios · Tailwind 3 + shadcn/ui(Radix)· lucide-react · sonner · react-dropzone · react-hook-form + zod + +## 目录结构 + +``` +src/ + apis/ # 接口函数,按域分文件(user/org/file/upload-config) + lib/http/ # axios 实例与拦截器(index.ts 导出 http 与常量) + routes/ + (user)/ # 用户端网盘页面(my/outside/inside/share/recycle) + admin/ # 管理后台;route.tsx 定义侧边菜单 + extract/ # 抽取 + components/ # 共享业务组件;ui/ 为 shadcn 基础组件 + hooks/ # 共享 hooks(kebab-case 文件名,如 use-upload-config.ts) + stores/user.ts # 登录态 store(persist,key 为 `${VITE_STORE_PREFIX}_user`) +``` + +### 路由/页面组织约定 + +- 功能目录 = `index.tsx`(路由 + `useColumns` + 页面组件)+ `-hooks/` + `-components/`;`-` 前缀目录不参与路由。 +- 列表页统一走 `useFetchList` 模式:`useState` 分页 `{pageIndex, pageSize}` + `useQuery`(queryKey 含 `[名称, pagination, params]`)+ `useReactTable`(`getCoreRowModel`、手动分页)+ 共享 ``/``。 +- 筛选条件采用「点击查询后生效」:草稿 state 与提交 params 分离,提交时过滤空值并重置到第一页(参考 `admin/org-account`、`admin/file-record`)。 +- 管理后台菜单在 `admin/route.tsx` 的 `menus` 中维护;仅平台机构(超管)可见的菜单加入 `platformOnlyUrls`(由 `useIsPlatformOrg` 即 `org.id === 1` 判断),机构级菜单(用户管理、上传配置)不加入。 + +## HTTP 层与后端约定(重要) + +- 响应拦截器(`src/lib/http/interceptors.ts`)resolve 的值是 **AlitaResult 本体**:`{ requestId, code, success, msg, data }`。因此 queryFn 里 `.then(res => res.data)` 拿到的才是业务数据(如分页 `Page`:`{ entityList, totalCount, pageCount }`)。 +- 分页参数走 query:`pageNo`(从 1 开始)/`pageSize`。 +- code `"200"` 成功;`555/666/999/401/402/700` 拦截器统一 `toast.error(msg)` 后 reject;`401/402` 清 token 并跳转 `/login?redirect=...`;`603` 换 token;非 JSON 响应按文件下载处理。 +- **错误提示只依赖拦截器**:mutation 用 `toast.promise(mutateAsync(...), { loading, success })`,**不要加 error 回调**(会与拦截器重复弹错)。 +- 请求自动携带 `Authorization: Bearer `(取自 user store)与 X-XSRF-TOKEN。 + +## 代码约定 + +- API 函数放 `src/apis/`,形如 `export const xxx = (data: any) => http.post("/path", data)`,参数类型沿用 `any`(存量风格)。 +- mutation hooks 放页面 `-hooks/` 下,仿照 `useCancelUser`/`useUserStatusActions`:`useMutation` + `toast.promise` 包装,暴露 `xxx` 与 `xxxIsPending`。 +- 表单弹窗优先 react-hook-form + zod(参考 `EditUserDialog`);简单确认弹窗仿 `CancelUserConfirmDialog`(Dialog + Spinner + onSuccess 里 `refetch` + `setOpen(false)`)。 +- 页面骨架:外层 `p-4 box-border min-h-full space-y-4` → 标题区(`text-lg` + `text-xs text-black/50` 副标题)→ 白卡片 `p-4 rounded-lg border bg-white space-y-2` → 工具行 → `` + sticky ``。 +- 图标用 lucide-react;文案为中文;时间格式 `yyyy-MM-dd HH:mm:ss`;文件大小用 `formatFileSize`(`src/lib/utils.ts`)。 + +## 环境变量(.env.development / .env.production) + +`VITE_API_URL`(dev 为 `/api`,由 vite `server.proxy` 转发到后端)、`VITE_STORE_PREFIX`、`VITE_BASEPATH`、`VITE_APP_TITLE`、`VITE_IS_INSIDE`(内网部署标识,影响菜单文案与路由行为)、`VITE_DOMAIN`、`VITE_INIT_PASSWORD`。 + +## 后端联调 + +- 接口文档在后端仓库 `alita-bjy-uygur/docs/` 下的 `接口文档-*.md`,动手前先读对应文档(含字段表、错误文案、权限说明)。 +- 管理端接口普遍要求「平台管理员/机构管理员」身份,前端仅做菜单/入口级控制,权限由后端兜底;错误文案以文档「校验失败提示」为准,前端校验提示需与后端 msg 保持一致。 +- 后端不可达时的冒烟技巧:登录态持久化在 localStorage `senmeshzc_user`,注入 `{"state":{"token":"...","user":{...},"org":{...}},"version":0}` 后刷新即可进入受限页面验证 UI(`org.id = 1` 即平台机构)。 + +## 验证清单 + +1. `bunx vite build && bunx tsc -b` 通过(新增路由时先 build 再 tsc)。 +2. `bun run dev` + 浏览器过一遍改动页面:菜单显隐、空态/加载/错误重试态、弹窗交互、toast 文案。 +3. 涉及上传/记录等平台差异逻辑时,分别验证内网(`VITE_IS_INSIDE=true`)口径。 diff --git a/src/components/reset-user-password-dialog.tsx b/src/components/reset-user-password-dialog.tsx index 62c1a46..edbf261 100644 --- a/src/components/reset-user-password-dialog.tsx +++ b/src/components/reset-user-password-dialog.tsx @@ -70,7 +70,8 @@ export function ResetUserPasswordDialog({ }, [open]); const onSubmit = (values: z.infer) => { - toast.promise(resetPasswordMutateAsync({ ...values, userId: data.id }), { + // 用户管理页行主键为 id,机构账号页行主键为 userId +toast.promise(resetPasswordMutateAsync({ ...values, userId: data.userId ?? data.id }), { loading: "提交中...", success: "修改成功", }); diff --git a/src/routes/admin/org-account/index.tsx b/src/routes/admin/org-account/index.tsx index c175c0c..ee7ae73 100644 --- a/src/routes/admin/org-account/index.tsx +++ b/src/routes/admin/org-account/index.tsx @@ -16,6 +16,7 @@ import { Pagination } from "@/components/pagination"; import { useFetchList } from "./-hooks/useFetchList"; import { useUserStatusActions } from "@/hooks/use-user-status-actions"; import { UserActionConfirmDialog } from "./-components/UserActionConfirmDialog"; +import { ResetUserPasswordDialog } from "@/components/reset-user-password-dialog"; import { useUserStore } from "@/stores/user"; import { DateRangePicker } from "@/components/date-range-picker"; @@ -38,6 +39,7 @@ export function useColumns({ handleEnable, handleDisable, handleCancelUser, + handleResetPassword, }: any) { const currentUserId = useUserStore((state: any) => state.user?.id); return [ @@ -131,6 +133,13 @@ export function useColumns({ } return (
+ {record.enabled ? (