feat: 项目总群自动建群+任务负责人后台视角+客户群活码进群+任务编辑
- 项目总群:≥3 人 appchat/create 自动建群(chatid=pgc+projectId),成员 diff 增删同步;DROP project_group_chats;推送改 appchat/send 群发、无群降级逐人 - 任务负责人:tasks.owner_userid(新建/编辑任务通讯录选人),task_set_owner 分配即预建 tgc 任务群发介绍,群主链接只归属负责人 - 任务编辑 task_update:基础字段+截图任务数量/关键词/说明,contents 调平只删 available 尾行 - 删除「一键推送」:task-one-click-push-service 整体移除(recordCardAck 移入 wecom-callback-events) - 负责人后台视角:users.wecom_userid 绑定,lib/owner-scope 行级过滤(undefined=全量/""=空范围/非空=过滤),~30 action 补 managerGate/taskOwnerGate,owner 登录仅见工作台/任务中心/数据回收/复盘报表 - 任务绑定客户群+进群活码:tasks 增 customer_chat_id/name、join_config_id、join_qr_code 4 列,join_way add/update/get/del(换绑走 update 保 config_id 活码不变),后台「客户群」弹窗,portal 认领后扫码自助进群(满员自动开新群) - 迁移 0013/0014/0015;催办支持按任务;企微可信域名 IP 验证文件 - 288 测试全绿;真机全链路验收通过(列群/活码四端点/portal 渲染/解绑清空)
This commit is contained in:
@@ -2,11 +2,13 @@
|
||||
|
||||
import { FormEvent, useCallback, useEffect, useState } from "react";
|
||||
import type { AuthUser, UserRole } from "../lib/user-auth";
|
||||
import { WecomUserPickerModal } from "./wecom-user-picker";
|
||||
|
||||
type ManagedUser = {
|
||||
id: string;
|
||||
username: string;
|
||||
role: UserRole;
|
||||
wecom_userid: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
@@ -31,6 +33,9 @@ export default function UsersPage({ currentUser }: { currentUser: AuthUser }) {
|
||||
const [resetTarget, setResetTarget] = useState<ManagedUser | null>(null);
|
||||
const [resetPassword, setResetPassword] = useState("");
|
||||
const [deleteTarget, setDeleteTarget] = useState<ManagedUser | null>(null);
|
||||
const [createWecom, setCreateWecom] = useState("");
|
||||
const [createPickerOpen, setCreatePickerOpen] = useState(false);
|
||||
const [bindTarget, setBindTarget] = useState<ManagedUser | null>(null);
|
||||
const [message, setMessage] = useState("");
|
||||
const [working, setWorking] = useState(false);
|
||||
|
||||
@@ -58,13 +63,20 @@ export default function UsersPage({ currentUser }: { currentUser: AuthUser }) {
|
||||
await fetch("/api/users", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ action: "create", username, password, role }),
|
||||
body: JSON.stringify({
|
||||
action: "create",
|
||||
username,
|
||||
password,
|
||||
role,
|
||||
wecomUserid: createWecom,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
setUsers(next);
|
||||
setUsername("");
|
||||
setPassword("");
|
||||
setRole("user");
|
||||
setCreateWecom("");
|
||||
setMessage("账号已创建,可以把账号和密码交给用户登录");
|
||||
} catch (reason) {
|
||||
setMessage(reason instanceof Error ? reason.message : "创建失败");
|
||||
@@ -126,6 +138,27 @@ export default function UsersPage({ currentUser }: { currentUser: AuthUser }) {
|
||||
}
|
||||
};
|
||||
|
||||
const bindWecom = async (userId: string, wecomUserid: string) => {
|
||||
setWorking(true);
|
||||
setMessage("");
|
||||
try {
|
||||
const next = await userApi(
|
||||
await fetch("/api/users", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ action: "set_wecom", userId, wecomUserid }),
|
||||
}),
|
||||
);
|
||||
setUsers(next);
|
||||
setBindTarget(null);
|
||||
setMessage(wecomUserid ? "企微绑定已更新,立即生效" : "已解除企微绑定");
|
||||
} catch (reason) {
|
||||
setMessage(reason instanceof Error ? reason.message : "绑定失败");
|
||||
} finally {
|
||||
setWorking(false);
|
||||
}
|
||||
};
|
||||
|
||||
const canReset = (item: ManagedUser) => {
|
||||
if (currentUser.role === "super_admin") {
|
||||
return item.role !== "super_admin" || item.id === currentUser.id;
|
||||
@@ -133,6 +166,11 @@ export default function UsersPage({ currentUser }: { currentUser: AuthUser }) {
|
||||
return item.role === "user";
|
||||
};
|
||||
|
||||
const canBind = (item: ManagedUser) => {
|
||||
if (item.role === "super_admin") return false;
|
||||
return currentUser.role === "super_admin" || item.role === "user";
|
||||
};
|
||||
|
||||
const canDelete = (item: ManagedUser) => {
|
||||
if (item.id === currentUser.id || item.role === "super_admin") return false;
|
||||
return currentUser.role === "super_admin" || item.role === "user";
|
||||
@@ -182,6 +220,22 @@ export default function UsersPage({ currentUser }: { currentUser: AuthUser }) {
|
||||
</select>
|
||||
</label>
|
||||
)}
|
||||
<label className="user-role-field">
|
||||
<span>企微 userid(任务负责人,可选)</span>
|
||||
<input
|
||||
value={createWecom}
|
||||
onChange={(event) => setCreateWecom(event.target.value)}
|
||||
placeholder="绑定后该账号只看自己负责的任务"
|
||||
autoComplete="off"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="ghost-button"
|
||||
onClick={() => setCreatePickerOpen(true)}
|
||||
>
|
||||
从企微选择
|
||||
</button>
|
||||
</label>
|
||||
<button className="primary-button" disabled={working}>
|
||||
{working ? "处理中…" : "创建账号"}
|
||||
</button>
|
||||
@@ -198,12 +252,45 @@ export default function UsersPage({ currentUser }: { currentUser: AuthUser }) {
|
||||
</div>
|
||||
<div className="user-table">
|
||||
<div className="user-table-head">
|
||||
<span>登录账号</span><span>角色</span><span>更新时间</span><span>操作</span>
|
||||
<span>登录账号</span><span>角色</span><span>企微绑定</span><span>更新时间</span><span>操作</span>
|
||||
</div>
|
||||
{users.map((item) => (
|
||||
<div className="user-table-row" key={item.id}>
|
||||
<strong>{item.username}</strong>
|
||||
<span className={`role-pill ${item.role}`}>{ROLE_LABEL[item.role]}</span>
|
||||
<span className="user-wecom-cell">
|
||||
{item.wecom_userid ? (
|
||||
<code>{item.wecom_userid}</code>
|
||||
) : (
|
||||
<span>—</span>
|
||||
)}
|
||||
{canBind(item) && (
|
||||
item.wecom_userid ? (
|
||||
<>
|
||||
<button
|
||||
className="user-action-button"
|
||||
onClick={() => setBindTarget(item)}
|
||||
>
|
||||
换绑
|
||||
</button>
|
||||
<button
|
||||
className="user-action-button danger"
|
||||
disabled={working}
|
||||
onClick={() => void bindWecom(item.id, "")}
|
||||
>
|
||||
解绑
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
className="user-action-button"
|
||||
onClick={() => setBindTarget(item)}
|
||||
>
|
||||
绑定
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
</span>
|
||||
<span>{new Date(item.updated_at).toLocaleDateString("zh-CN", { timeZone: "Asia/Shanghai" })}</span>
|
||||
<div className="user-row-actions">
|
||||
{canReset(item) && (
|
||||
@@ -307,6 +394,28 @@ export default function UsersPage({ currentUser }: { currentUser: AuthUser }) {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{createPickerOpen && (
|
||||
<WecomUserPickerModal
|
||||
mode="single"
|
||||
selected={createWecom ? [createWecom] : []}
|
||||
onClose={() => setCreatePickerOpen(false)}
|
||||
onConfirm={(userids) => {
|
||||
setCreateWecom(userids[0] ?? "");
|
||||
setCreatePickerOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{bindTarget && (
|
||||
<WecomUserPickerModal
|
||||
mode="single"
|
||||
selected={bindTarget.wecom_userid ? [bindTarget.wecom_userid] : []}
|
||||
onClose={() => setBindTarget(null)}
|
||||
onConfirm={(userids) => {
|
||||
void bindWecom(bindTarget.id, userids[0] ?? "");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user