mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-06 01:56:37 +08:00
feat(user): support user-group assignment in user management
This commit is contained in:
@@ -89,6 +89,7 @@ func (h *Handler) Register(mux *http.ServeMux) {
|
|||||||
mux.HandleFunc("/api/v1/user/update", h.userUpdate)
|
mux.HandleFunc("/api/v1/user/update", h.userUpdate)
|
||||||
mux.HandleFunc("/api/v1/user/delete", h.userDelete)
|
mux.HandleFunc("/api/v1/user/delete", h.userDelete)
|
||||||
mux.HandleFunc("/api/v1/user/reset", h.userResetFlow)
|
mux.HandleFunc("/api/v1/user/reset", h.userResetFlow)
|
||||||
|
mux.HandleFunc("/api/v1/user/groups", h.userGroups)
|
||||||
mux.HandleFunc("/api/v1/config/get", h.getConfigByName)
|
mux.HandleFunc("/api/v1/config/get", h.getConfigByName)
|
||||||
mux.HandleFunc("/api/v1/config/list", h.getConfigs)
|
mux.HandleFunc("/api/v1/config/list", h.getConfigs)
|
||||||
mux.HandleFunc("/api/v1/config/update", h.updateConfigs)
|
mux.HandleFunc("/api/v1/config/update", h.updateConfigs)
|
||||||
|
|||||||
@@ -60,10 +60,21 @@ func (h *Handler) userCreate(w http.ResponseWriter, r *http.Request) {
|
|||||||
roleID := 1
|
roleID := 1
|
||||||
now := time.Now().UnixMilli()
|
now := time.Now().UnixMilli()
|
||||||
|
|
||||||
if err := h.repo.CreateUser(username, security.MD5(pwd), roleID, expTime, flow, flowResetTime, num, status, now); err != nil {
|
userID, err := h.repo.CreateUser(username, security.MD5(pwd), roleID, expTime, flow, flowResetTime, num, status, now)
|
||||||
|
if err != nil {
|
||||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
groupIDs := asInt64Slice(req["groupIds"])
|
||||||
|
if len(groupIDs) > 0 {
|
||||||
|
if addErr := h.repo.AddUserToGroups(userID, groupIDs, now); addErr == nil {
|
||||||
|
for _, gid := range groupIDs {
|
||||||
|
_ = h.syncPermissionsByUserGroup(gid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
response.WriteJSON(w, response.OKEmpty())
|
response.WriteJSON(w, response.OKEmpty())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,9 +144,36 @@ func (h *Handler) userUpdate(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
h.repo.PropagateUserFlowToTunnels(id, flow, num, expTime, flowResetTime)
|
h.repo.PropagateUserFlowToTunnels(id, flow, num, expTime, flowResetTime)
|
||||||
|
|
||||||
|
if groupIDsRaw, ok := req["groupIds"]; ok {
|
||||||
|
newGroupIDs := asInt64Slice(groupIDsRaw)
|
||||||
|
if affected, replaceErr := h.repo.ReplaceUserGroupsByUserID(id, newGroupIDs, now); replaceErr == nil {
|
||||||
|
for _, gid := range affected {
|
||||||
|
_ = h.syncPermissionsByUserGroup(gid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
response.WriteJSON(w, response.OKEmpty())
|
response.WriteJSON(w, response.OKEmpty())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) userGroups(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
response.WriteJSON(w, response.ErrDefault("请求失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id := idFromBody(r, w)
|
||||||
|
if id <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ids, err := h.repo.GetUserGroupIDsByUserID(id)
|
||||||
|
if err != nil {
|
||||||
|
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.WriteJSON(w, response.OK(ids))
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) userDelete(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) userDelete(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
response.WriteJSON(w, response.ErrDefault("请求失败"))
|
response.WriteJSON(w, response.ErrDefault("请求失败"))
|
||||||
@@ -3204,3 +3242,17 @@ func randomToken(n int) string {
|
|||||||
}
|
}
|
||||||
return hex.EncodeToString(buf)
|
return hex.EncodeToString(buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func asInt64Slice(v interface{}) []int64 {
|
||||||
|
arr := asAnySlice(v)
|
||||||
|
if len(arr) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ids := make([]int64, 0, len(arr))
|
||||||
|
for _, x := range arr {
|
||||||
|
if id := asInt64(x, 0); id > 0 {
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
|||||||
@@ -50,8 +50,17 @@ func (r *Repository) ListGroupPermissionPairsByUserGroup(userGroupID int64) ([][
|
|||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListGroupPermissionPairsByTunnelGroup returns [userGroupID, tunnelGroupID] pairs
|
func (r *Repository) GetUserGroupIDsByUserID(userID int64) ([]int64, error) {
|
||||||
// for all group permissions associated with a tunnel group.
|
if r == nil || r.db == nil {
|
||||||
|
return nil, errors.New("repository not initialized")
|
||||||
|
}
|
||||||
|
var ids []int64
|
||||||
|
err := r.db.Model(&model.UserGroupUser{}).
|
||||||
|
Where("user_id = ?", userID).
|
||||||
|
Pluck("user_group_id", &ids).Error
|
||||||
|
return ids, err
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Repository) ListGroupPermissionPairsByTunnelGroup(tunnelGroupID int64) ([][2]int64, error) {
|
func (r *Repository) ListGroupPermissionPairsByTunnelGroup(tunnelGroupID int64) ([][2]int64, error) {
|
||||||
if r == nil || r.db == nil {
|
if r == nil || r.db == nil {
|
||||||
return nil, errors.New("repository not initialized")
|
return nil, errors.New("repository not initialized")
|
||||||
|
|||||||
@@ -34,9 +34,9 @@ func (r *Repository) UserExistsExcluding(username string, excludeID int64) (bool
|
|||||||
return cnt > 0, err
|
return cnt > 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) CreateUser(username, pwdHash string, roleID int, expTime, flow, flowResetTime int64, num, status int, now int64) error {
|
func (r *Repository) CreateUser(username, pwdHash string, roleID int, expTime, flow, flowResetTime int64, num, status int, now int64) (int64, error) {
|
||||||
if r == nil || r.db == nil {
|
if r == nil || r.db == nil {
|
||||||
return errors.New("repository not initialized")
|
return 0, errors.New("repository not initialized")
|
||||||
}
|
}
|
||||||
user := model.User{
|
user := model.User{
|
||||||
User: username,
|
User: username,
|
||||||
@@ -52,7 +52,10 @@ func (r *Repository) CreateUser(username, pwdHash string, roleID int, expTime, f
|
|||||||
UpdatedTime: sql.NullInt64{Int64: now, Valid: true},
|
UpdatedTime: sql.NullInt64{Int64: now, Valid: true},
|
||||||
Status: status,
|
Status: status,
|
||||||
}
|
}
|
||||||
return r.db.Create(&user).Error
|
if err := r.db.Create(&user).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return user.ID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) GetUserRoleID(userID int64) (int, error) {
|
func (r *Repository) GetUserRoleID(userID int64) (int, error) {
|
||||||
@@ -1406,3 +1409,70 @@ func parsePortRangeSpec(input string) []int {
|
|||||||
sort.Ints(out)
|
sort.Ints(out)
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Repository) AddUserToGroups(userID int64, groupIDs []int64, now int64) error {
|
||||||
|
if r == nil || r.db == nil {
|
||||||
|
return errors.New("repository not initialized")
|
||||||
|
}
|
||||||
|
if len(groupIDs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
rows := make([]model.UserGroupUser, 0, len(groupIDs))
|
||||||
|
for _, gid := range groupIDs {
|
||||||
|
if gid <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rows = append(rows, model.UserGroupUser{UserGroupID: gid, UserID: userID, CreatedTime: now})
|
||||||
|
}
|
||||||
|
if len(rows) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return r.db.Clauses(clause.OnConflict{DoNothing: true}).Create(&rows).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) ReplaceUserGroupsByUserID(userID int64, newGroupIDs []int64, now int64) (affectedGroupIDs []int64, err error) {
|
||||||
|
if r == nil || r.db == nil {
|
||||||
|
return nil, errors.New("repository not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
var oldGroupIDs []int64
|
||||||
|
if err = r.db.Model(&model.UserGroupUser{}).
|
||||||
|
Where("user_id = ?", userID).
|
||||||
|
Pluck("user_group_id", &oldGroupIDs).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
seen := make(map[int64]struct{})
|
||||||
|
for _, id := range oldGroupIDs {
|
||||||
|
seen[id] = struct{}{}
|
||||||
|
}
|
||||||
|
for _, id := range newGroupIDs {
|
||||||
|
if id > 0 {
|
||||||
|
seen[id] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for id := range seen {
|
||||||
|
affectedGroupIDs = append(affectedGroupIDs, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = r.db.Where("user_id = ?", userID).Delete(&model.UserGroupUser{}).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(newGroupIDs) == 0 {
|
||||||
|
return affectedGroupIDs, nil
|
||||||
|
}
|
||||||
|
rows := make([]model.UserGroupUser, 0, len(newGroupIDs))
|
||||||
|
for _, gid := range newGroupIDs {
|
||||||
|
if gid <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rows = append(rows, model.UserGroupUser{UserGroupID: gid, UserID: userID, CreatedTime: now})
|
||||||
|
}
|
||||||
|
if len(rows) > 0 {
|
||||||
|
if err = r.db.Clauses(clause.OnConflict{DoNothing: true}).Create(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return affectedGroupIDs, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -131,6 +131,9 @@ export const updatePassword = (data: any) =>
|
|||||||
export const resetUserFlow = (data: { id: number; type: number }) =>
|
export const resetUserFlow = (data: { id: number; type: number }) =>
|
||||||
Network.post("/user/reset", data);
|
Network.post("/user/reset", data);
|
||||||
|
|
||||||
|
export const getUserGroups = (id: number) =>
|
||||||
|
Network.post<number[]>("/user/groups", { id });
|
||||||
|
|
||||||
// 网站配置相关接口
|
// 网站配置相关接口
|
||||||
export const getConfigs = () => Network.post("/config/list");
|
export const getConfigs = () => Network.post("/config/list");
|
||||||
export const getConfigByName = (name: string) =>
|
export const getConfigByName = (name: string) =>
|
||||||
@@ -141,8 +144,10 @@ export const updateConfig = (name: string, value: string) =>
|
|||||||
Network.post("/config/update-single", { name, value });
|
Network.post("/config/update-single", { name, value });
|
||||||
|
|
||||||
export const exportBackupData = () => Network.post("/backup/export");
|
export const exportBackupData = () => Network.post("/backup/export");
|
||||||
export const importBackupData = (data: any) => Network.post("/backup/import", data);
|
export const importBackupData = (data: any) =>
|
||||||
export const restoreBackupData = (data: any) => Network.post("/backup/restore", data);
|
Network.post("/backup/import", data);
|
||||||
|
export const restoreBackupData = (data: any) =>
|
||||||
|
Network.post("/backup/restore", data);
|
||||||
|
|
||||||
// 验证码相关接口
|
// 验证码相关接口
|
||||||
export const checkCaptcha = () => Network.post("/captcha/check");
|
export const checkCaptcha = () => Network.post("/captcha/check");
|
||||||
@@ -291,5 +296,7 @@ export interface AnnouncementData {
|
|||||||
enabled: number;
|
enabled: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getAnnouncement = () => Network.get<AnnouncementData>("/announcement/get");
|
export const getAnnouncement = () =>
|
||||||
export const updateAnnouncement = (data: AnnouncementData) => Network.post("/announcement/update", data);
|
Network.get<AnnouncementData>("/announcement/get");
|
||||||
|
export const updateAnnouncement = (data: AnnouncementData) =>
|
||||||
|
Network.post("/announcement/update", data);
|
||||||
|
|||||||
@@ -18,7 +18,14 @@ import {
|
|||||||
} from "@heroui/modal";
|
} from "@heroui/modal";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
import { updateConfigs, exportBackup, importBackup, getAnnouncement, updateAnnouncement, type AnnouncementData } from "@/api";
|
import {
|
||||||
|
updateConfigs,
|
||||||
|
exportBackup,
|
||||||
|
importBackup,
|
||||||
|
getAnnouncement,
|
||||||
|
updateAnnouncement,
|
||||||
|
type AnnouncementData,
|
||||||
|
} from "@/api";
|
||||||
import { SettingsIcon } from "@/components/icons";
|
import { SettingsIcon } from "@/components/icons";
|
||||||
import { isAdmin } from "@/utils/auth";
|
import { isAdmin } from "@/utils/auth";
|
||||||
import {
|
import {
|
||||||
@@ -501,7 +508,9 @@ export default function ConfigPage() {
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
onPress={() => setTypes(allSelected ? [] : [...BACKUP_TYPE_VALUES])}
|
onPress={() =>
|
||||||
|
setTypes(allSelected ? [] : [...BACKUP_TYPE_VALUES])
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{allSelected ? "取消全选" : "全选"}
|
{allSelected ? "取消全选" : "全选"}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -675,7 +684,10 @@ export default function ConfigPage() {
|
|||||||
<Switch
|
<Switch
|
||||||
isSelected={announcement.enabled === 1}
|
isSelected={announcement.enabled === 1}
|
||||||
onValueChange={(checked) =>
|
onValueChange={(checked) =>
|
||||||
setAnnouncement({ ...announcement, enabled: checked ? 1 : 0 })
|
setAnnouncement({
|
||||||
|
...announcement,
|
||||||
|
enabled: checked ? 1 : 0,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span className="text-sm text-gray-700 dark:text-gray-300">
|
<span className="text-sm text-gray-700 dark:text-gray-300">
|
||||||
@@ -689,10 +701,10 @@ export default function ConfigPage() {
|
|||||||
|
|
||||||
<Textarea
|
<Textarea
|
||||||
label="公告内容"
|
label="公告内容"
|
||||||
|
minRows={4}
|
||||||
placeholder="请输入公告内容"
|
placeholder="请输入公告内容"
|
||||||
value={announcement.content}
|
value={announcement.content}
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
minRows={4}
|
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setAnnouncement({ ...announcement, content: e.target.value })
|
setAnnouncement({ ...announcement, content: e.target.value })
|
||||||
}
|
}
|
||||||
@@ -801,7 +813,11 @@ export default function ConfigPage() {
|
|||||||
<Button variant="light" onPress={onClose}>
|
<Button variant="light" onPress={onClose}>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="primary" isLoading={exporting} onPress={handleExport}>
|
<Button
|
||||||
|
color="primary"
|
||||||
|
isLoading={exporting}
|
||||||
|
onPress={handleExport}
|
||||||
|
>
|
||||||
{exporting ? "导出中..." : "确认导出"}
|
{exporting ? "导出中..." : "确认导出"}
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
|
|||||||
@@ -13,7 +13,11 @@ import {
|
|||||||
ResponsiveContainer,
|
ResponsiveContainer,
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
|
|
||||||
import { getUserPackageInfo, getAnnouncement, type AnnouncementData } from "@/api";
|
import {
|
||||||
|
getUserPackageInfo,
|
||||||
|
getAnnouncement,
|
||||||
|
type AnnouncementData,
|
||||||
|
} from "@/api";
|
||||||
|
|
||||||
interface UserInfo {
|
interface UserInfo {
|
||||||
flow: number;
|
flow: number;
|
||||||
@@ -71,7 +75,9 @@ export default function DashboardPage() {
|
|||||||
const [forwardList, setForwardList] = useState<Forward[]>([]);
|
const [forwardList, setForwardList] = useState<Forward[]>([]);
|
||||||
const [statisticsFlows, setStatisticsFlows] = useState<StatisticsFlow[]>([]);
|
const [statisticsFlows, setStatisticsFlows] = useState<StatisticsFlow[]>([]);
|
||||||
const [isAdmin, setIsAdmin] = useState(false);
|
const [isAdmin, setIsAdmin] = useState(false);
|
||||||
const [announcement, setAnnouncement] = useState<AnnouncementData | null>(null);
|
const [announcement, setAnnouncement] = useState<AnnouncementData | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
const [addressModalOpen, setAddressModalOpen] = useState(false);
|
const [addressModalOpen, setAddressModalOpen] = useState(false);
|
||||||
const [addressModalTitle, setAddressModalTitle] = useState("");
|
const [addressModalTitle, setAddressModalTitle] = useState("");
|
||||||
|
|||||||
@@ -317,22 +317,22 @@ export default function TunnelPage() {
|
|||||||
|
|
||||||
// 直接使用列表数据,getAllTunnels 已经包含完整的节点信息
|
// 直接使用列表数据,getAllTunnels 已经包含完整的节点信息
|
||||||
setForm({
|
setForm({
|
||||||
id: tunnel.id,
|
id: tunnel.id,
|
||||||
name: tunnel.name,
|
name: tunnel.name,
|
||||||
type: tunnel.type,
|
type: tunnel.type,
|
||||||
inNodeId: tunnel.inNodeId || [],
|
inNodeId: tunnel.inNodeId || [],
|
||||||
outNodeId: tunnel.outNodeId || [],
|
outNodeId: tunnel.outNodeId || [],
|
||||||
chainNodes: tunnel.chainNodes || [],
|
chainNodes: tunnel.chainNodes || [],
|
||||||
flow: tunnel.flow,
|
flow: tunnel.flow,
|
||||||
trafficRatio: tunnel.trafficRatio,
|
trafficRatio: tunnel.trafficRatio,
|
||||||
inIp: tunnel.inIp
|
inIp: tunnel.inIp
|
||||||
? tunnel.inIp
|
? tunnel.inIp
|
||||||
.split(",")
|
.split(",")
|
||||||
.map((ip: string) => ip.trim())
|
.map((ip: string) => ip.trim())
|
||||||
.join("\n")
|
.join("\n")
|
||||||
: "",
|
: "",
|
||||||
ipPreference: tunnel.ipPreference || "",
|
ipPreference: tunnel.ipPreference || "",
|
||||||
status: tunnel.status,
|
status: tunnel.status,
|
||||||
});
|
});
|
||||||
setErrors({});
|
setErrors({});
|
||||||
setModalOpen(true);
|
setModalOpen(true);
|
||||||
@@ -1050,7 +1050,9 @@ export default function TunnelPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 流量配置 */}
|
{/* 流量配置 */}
|
||||||
<div className={`grid gap-2 ${tunnel.type === 2 && tunnel.ipPreference ? "grid-cols-3" : "grid-cols-2"}`}>
|
<div
|
||||||
|
className={`grid gap-2 ${tunnel.type === 2 && tunnel.ipPreference ? "grid-cols-3" : "grid-cols-2"}`}
|
||||||
|
>
|
||||||
<div className="text-center p-1.5 bg-default-50 dark:bg-default-100/30 rounded">
|
<div className="text-center p-1.5 bg-default-50 dark:bg-default-100/30 rounded">
|
||||||
<div className="text-xs text-default-500">
|
<div className="text-xs text-default-500">
|
||||||
流量计算
|
流量计算
|
||||||
@@ -1073,7 +1075,9 @@ export default function TunnelPage() {
|
|||||||
连接偏好
|
连接偏好
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm font-semibold text-foreground mt-0.5">
|
<div className="text-sm font-semibold text-foreground mt-0.5">
|
||||||
{tunnel.ipPreference === "v4" ? "IPv4" : "IPv6"}
|
{tunnel.ipPreference === "v4"
|
||||||
|
? "IPv4"
|
||||||
|
: "IPv6"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -1280,11 +1284,11 @@ export default function TunnelPage() {
|
|||||||
errorMessage={errors.trafficRatio}
|
errorMessage={errors.trafficRatio}
|
||||||
isInvalid={!!errors.trafficRatio}
|
isInvalid={!!errors.trafficRatio}
|
||||||
label="流量倍率"
|
label="流量倍率"
|
||||||
placeholder="例如:0.5 或 1 或 2"
|
|
||||||
type="number"
|
|
||||||
step="any"
|
|
||||||
min={0.01}
|
|
||||||
max={100}
|
max={100}
|
||||||
|
min={0.01}
|
||||||
|
placeholder="例如:0.5 或 1 或 2"
|
||||||
|
step="any"
|
||||||
|
type="number"
|
||||||
value={form.trafficRatio.toString()}
|
value={form.trafficRatio.toString()}
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import { parseDate } from "@internationalized/date";
|
|||||||
import {
|
import {
|
||||||
User,
|
User,
|
||||||
UserForm,
|
UserForm,
|
||||||
|
UserGroup,
|
||||||
UserTunnel,
|
UserTunnel,
|
||||||
TunnelAssignItem,
|
TunnelAssignItem,
|
||||||
Tunnel,
|
Tunnel,
|
||||||
@@ -49,6 +50,8 @@ import {
|
|||||||
updateUserTunnel,
|
updateUserTunnel,
|
||||||
getSpeedLimitList,
|
getSpeedLimitList,
|
||||||
resetUserFlow,
|
resetUserFlow,
|
||||||
|
getUserGroupList,
|
||||||
|
getUserGroups,
|
||||||
} from "@/api";
|
} from "@/api";
|
||||||
import {
|
import {
|
||||||
SearchIcon,
|
SearchIcon,
|
||||||
@@ -204,12 +207,14 @@ export default function UserPage() {
|
|||||||
// 其他数据
|
// 其他数据
|
||||||
const [tunnels, setTunnels] = useState<Tunnel[]>([]);
|
const [tunnels, setTunnels] = useState<Tunnel[]>([]);
|
||||||
const [speedLimits, setSpeedLimits] = useState<SpeedLimit[]>([]);
|
const [speedLimits, setSpeedLimits] = useState<SpeedLimit[]>([]);
|
||||||
|
const [userGroups, setUserGroups] = useState<UserGroup[]>([]);
|
||||||
|
|
||||||
// 生命周期
|
// 生命周期
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadUsers();
|
loadUsers();
|
||||||
loadTunnels();
|
loadTunnels();
|
||||||
loadSpeedLimits();
|
loadSpeedLimits();
|
||||||
|
loadUserGroups();
|
||||||
}, [pagination.current, pagination.size, searchKeyword]);
|
}, [pagination.current, pagination.size, searchKeyword]);
|
||||||
|
|
||||||
// 数据加载函数
|
// 数据加载函数
|
||||||
@@ -256,6 +261,16 @@ export default function UserPage() {
|
|||||||
} catch {}
|
} catch {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadUserGroups = async () => {
|
||||||
|
try {
|
||||||
|
const response = await getUserGroupList();
|
||||||
|
|
||||||
|
if (response.code === 0) {
|
||||||
|
setUserGroups(response.data || []);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
};
|
||||||
|
|
||||||
const loadUserTunnels = async (userId: number) => {
|
const loadUserTunnels = async (userId: number) => {
|
||||||
setTunnelListLoading(true);
|
setTunnelListLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -289,12 +304,23 @@ export default function UserPage() {
|
|||||||
num: 10,
|
num: 10,
|
||||||
expTime: null,
|
expTime: null,
|
||||||
flowResetTime: 0,
|
flowResetTime: 0,
|
||||||
|
groupIds: [],
|
||||||
});
|
});
|
||||||
onUserModalOpen();
|
onUserModalOpen();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEdit = (user: User) => {
|
const handleEdit = async (user: User) => {
|
||||||
setIsEdit(true);
|
setIsEdit(true);
|
||||||
|
let currentGroupIds: number[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const groupRes = await getUserGroups(user.id);
|
||||||
|
|
||||||
|
if (groupRes.code === 0) {
|
||||||
|
currentGroupIds = groupRes.data || [];
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
setUserForm({
|
setUserForm({
|
||||||
id: user.id,
|
id: user.id,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
@@ -305,6 +331,7 @@ export default function UserPage() {
|
|||||||
num: user.num,
|
num: user.num,
|
||||||
expTime: user.expTime ? new Date(user.expTime) : null,
|
expTime: user.expTime ? new Date(user.expTime) : null,
|
||||||
flowResetTime: user.flowResetTime ?? 0,
|
flowResetTime: user.flowResetTime ?? 0,
|
||||||
|
groupIds: currentGroupIds,
|
||||||
});
|
});
|
||||||
onUserModalOpen();
|
onUserModalOpen();
|
||||||
};
|
};
|
||||||
@@ -345,6 +372,7 @@ export default function UserPage() {
|
|||||||
const submitData: any = {
|
const submitData: any = {
|
||||||
...userForm,
|
...userForm,
|
||||||
expTime: userForm.expTime.getTime(),
|
expTime: userForm.expTime.getTime(),
|
||||||
|
groupIds: userForm.groupIds ?? [],
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isEdit && !submitData.pwd) {
|
if (isEdit && !submitData.pwd) {
|
||||||
@@ -951,6 +979,26 @@ export default function UserPage() {
|
|||||||
<Radio value="1">正常</Radio>
|
<Radio value="1">正常</Radio>
|
||||||
<Radio value="0">禁用</Radio>
|
<Radio value="0">禁用</Radio>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
|
|
||||||
|
{userGroups.length > 0 && (
|
||||||
|
<Select
|
||||||
|
label="用户分组(可选)"
|
||||||
|
placeholder="选择要加入的分组"
|
||||||
|
selectedKeys={new Set((userForm.groupIds ?? []).map(String))}
|
||||||
|
selectionMode="multiple"
|
||||||
|
onSelectionChange={(keys) => {
|
||||||
|
const selected = Array.from(keys as Set<string>).map(Number);
|
||||||
|
|
||||||
|
setUserForm((prev) => ({ ...prev, groupIds: selected }));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{userGroups.map((g) => (
|
||||||
|
<SelectItem key={g.id.toString()} textValue={g.name}>
|
||||||
|
{g.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button onPress={onUserModalClose}>取消</Button>
|
<Button onPress={onUserModalClose}>取消</Button>
|
||||||
|
|||||||
@@ -20,6 +20,12 @@ export interface User {
|
|||||||
outFlow?: number; // 上传流量(字节)
|
outFlow?: number; // 上传流量(字节)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserGroup {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
status: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface UserForm {
|
export interface UserForm {
|
||||||
id?: number;
|
id?: number;
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -30,6 +36,7 @@ export interface UserForm {
|
|||||||
num: number;
|
num: number;
|
||||||
expTime: Date | null;
|
expTime: Date | null;
|
||||||
flowResetTime: number;
|
flowResetTime: number;
|
||||||
|
groupIds?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserTunnel {
|
export interface UserTunnel {
|
||||||
|
|||||||
Reference in New Issue
Block a user