feat: 机构账号页新增重置密码;弹窗组件兼容 userId/id 行结构;代理切回公司地址
This commit is contained in:
88
AGENTS.md
Normal file
88
AGENTS.md
Normal file
@@ -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`、手动分页)+ 共享 `<DataTable>`/`<Pagination>`。
|
||||||
|
- 筛选条件采用「点击查询后生效」:草稿 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 <token>`(取自 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` → 工具行 → `<DataTable>` + sticky `<Pagination>`。
|
||||||
|
- 图标用 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`)口径。
|
||||||
@@ -70,7 +70,8 @@ export function ResetUserPasswordDialog({
|
|||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
const onSubmit = (values: z.infer<typeof formSchema>) => {
|
const onSubmit = (values: z.infer<typeof formSchema>) => {
|
||||||
toast.promise(resetPasswordMutateAsync({ ...values, userId: data.id }), {
|
// 用户管理页行主键为 id,机构账号页行主键为 userId
|
||||||
|
toast.promise(resetPasswordMutateAsync({ ...values, userId: data.userId ?? data.id }), {
|
||||||
loading: "提交中...",
|
loading: "提交中...",
|
||||||
success: "修改成功",
|
success: "修改成功",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { Pagination } from "@/components/pagination";
|
|||||||
import { useFetchList } from "./-hooks/useFetchList";
|
import { useFetchList } from "./-hooks/useFetchList";
|
||||||
import { useUserStatusActions } from "@/hooks/use-user-status-actions";
|
import { useUserStatusActions } from "@/hooks/use-user-status-actions";
|
||||||
import { UserActionConfirmDialog } from "./-components/UserActionConfirmDialog";
|
import { UserActionConfirmDialog } from "./-components/UserActionConfirmDialog";
|
||||||
|
import { ResetUserPasswordDialog } from "@/components/reset-user-password-dialog";
|
||||||
import { useUserStore } from "@/stores/user";
|
import { useUserStore } from "@/stores/user";
|
||||||
import { DateRangePicker } from "@/components/date-range-picker";
|
import { DateRangePicker } from "@/components/date-range-picker";
|
||||||
|
|
||||||
@@ -38,6 +39,7 @@ export function useColumns({
|
|||||||
handleEnable,
|
handleEnable,
|
||||||
handleDisable,
|
handleDisable,
|
||||||
handleCancelUser,
|
handleCancelUser,
|
||||||
|
handleResetPassword,
|
||||||
}: any) {
|
}: any) {
|
||||||
const currentUserId = useUserStore((state: any) => state.user?.id);
|
const currentUserId = useUserStore((state: any) => state.user?.id);
|
||||||
return [
|
return [
|
||||||
@@ -131,6 +133,13 @@ export function useColumns({
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="space-x-2">
|
<div className="space-x-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => handleResetPassword(record)}
|
||||||
|
>
|
||||||
|
重置密码
|
||||||
|
</Button>
|
||||||
{record.enabled ? (
|
{record.enabled ? (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -189,11 +198,20 @@ function RouteComponent() {
|
|||||||
setOpenConfirmDialog(true);
|
setOpenConfirmDialog(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 重置密码
|
||||||
|
const [openResetPasswordDialog, setOpenResetPasswordDialog] =
|
||||||
|
useState(false);
|
||||||
|
const handleResetPassword = (record: any) => {
|
||||||
|
setCurrentEntity(record);
|
||||||
|
setOpenResetPasswordDialog(true);
|
||||||
|
};
|
||||||
|
|
||||||
const { table, query, search } = useFetchList(() =>
|
const { table, query, search } = useFetchList(() =>
|
||||||
useColumns({
|
useColumns({
|
||||||
handleEnable,
|
handleEnable,
|
||||||
handleDisable,
|
handleDisable,
|
||||||
handleCancelUser,
|
handleCancelUser,
|
||||||
|
handleResetPassword,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -229,6 +247,11 @@ function RouteComponent() {
|
|||||||
data={currentEntity}
|
data={currentEntity}
|
||||||
refetch={query?.refetch}
|
refetch={query?.refetch}
|
||||||
/>
|
/>
|
||||||
|
<ResetUserPasswordDialog
|
||||||
|
open={openResetPasswordDialog}
|
||||||
|
setOpen={setOpenResetPasswordDialog}
|
||||||
|
data={currentEntity}
|
||||||
|
/>
|
||||||
<div className="p-4 box-border min-h-full space-y-4">
|
<div className="p-4 box-border min-h-full space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-lg">机构账号</div>
|
<div className="text-lg">机构账号</div>
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ export default defineConfig({
|
|||||||
proxy: {
|
proxy: {
|
||||||
"/api": {
|
"/api": {
|
||||||
// target: "http://192.168.71.2:8080/", // 家
|
// target: "http://192.168.71.2:8080/", // 家
|
||||||
// target: "http://192.168.1.36:8080/", // 公司
|
target: "http://192.168.1.36:8080/", // 公司
|
||||||
target: "http://localhost:8080/", // 本地Docker测试
|
// target: "http://localhost:8080/", // 本地Docker测试
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: (path) => path.replace(/^\/api/, ""),
|
rewrite: (path) => path.replace(/^\/api/, ""),
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user