update:部分样式优化
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import { ChevronDown } from "lucide-react";
|
||||||
import type { Employee, Role, Workspace } from "../../services/api";
|
import type { Employee, Role, Workspace } from "../../services/api";
|
||||||
import {
|
import {
|
||||||
AppFormDialog,
|
AppFormDialog,
|
||||||
@@ -59,6 +61,18 @@ export function UserFormDialog({
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onResetPassword?: () => void;
|
onResetPassword?: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
const roleOptions = useMemo(
|
||||||
|
() => (editing ? roles : createRoleOptions),
|
||||||
|
[editing, roles, createRoleOptions],
|
||||||
|
);
|
||||||
|
const [roleMenuOpen, setRoleMenuOpen] = useState(false);
|
||||||
|
const roleDisabled = editing
|
||||||
|
? editing.user_id === currentUserId || roles.length === 0
|
||||||
|
: createRoleOptions.length === 0;
|
||||||
|
const selectedRole =
|
||||||
|
roleOptions.find((role) => role.role_code === form.role_code) ??
|
||||||
|
roleOptions[0];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppFormDialog
|
<AppFormDialog
|
||||||
open={open}
|
open={open}
|
||||||
@@ -122,45 +136,54 @@ export function UserFormDialog({
|
|||||||
</label>
|
</label>
|
||||||
<label className={formFieldClass}>
|
<label className={formFieldClass}>
|
||||||
<span>角色</span>
|
<span>角色</span>
|
||||||
<select
|
<div className="relative">
|
||||||
className={formInputClass}
|
<button
|
||||||
value={form.role_code}
|
type="button"
|
||||||
disabled={
|
disabled={roleDisabled}
|
||||||
editing
|
className={`${formInputClass} flex items-center justify-between gap-2 ${
|
||||||
? editing.user_id === currentUserId || roles.length === 0
|
roleDisabled ? "cursor-not-allowed opacity-50" : "cursor-pointer"
|
||||||
: createRoleOptions.length === 0
|
}`}
|
||||||
}
|
onClick={() => !roleDisabled && setRoleMenuOpen((open) => !open)}
|
||||||
onChange={(event) =>
|
>
|
||||||
|
<span className="truncate text-left">
|
||||||
|
{selectedRole ? selectedRole.role_name : "加载中…"}
|
||||||
|
</span>
|
||||||
|
<ChevronDown
|
||||||
|
size={15}
|
||||||
|
className={`shrink-0 text-[#7890a8] transition-transform ${
|
||||||
|
roleMenuOpen ? "rotate-180" : "rotate-0"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
{roleMenuOpen && !roleDisabled && (
|
||||||
|
<div className="absolute left-0 right-0 z-30 mt-1 overflow-hidden rounded-md border border-[#d6e0e9] bg-white shadow-lg">
|
||||||
|
<div className="max-h-[180px] overflow-y-auto">
|
||||||
|
{roleOptions.map((role) => {
|
||||||
|
const isSelected = role.role_code === form.role_code;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={role.role_code}
|
||||||
|
type="button"
|
||||||
|
className={`flex w-full items-center justify-between border-0 bg-white px-3 py-2 text-left text-[12px] text-[#2b4056] hover:bg-[#f3f7fb] ${
|
||||||
|
isSelected ? "bg-[#edf5ff] font-semibold text-[#176cc0]" : ""
|
||||||
|
}`}
|
||||||
|
onClick={() => {
|
||||||
onChangeForm({
|
onChangeForm({
|
||||||
...form,
|
...form,
|
||||||
role_code: event.target.value as UserFormState["role_code"],
|
role_code: role.role_code as UserFormState["role_code"],
|
||||||
})
|
});
|
||||||
}
|
setRoleMenuOpen(false);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{editing ? (
|
<span className="truncate">{role.role_name}</span>
|
||||||
roles.length === 0 ? (
|
{isSelected && <span className="ml-2 text-[11px]">✓</span>}
|
||||||
<option value="" disabled>
|
</button>
|
||||||
加载中…
|
);
|
||||||
</option>
|
})}
|
||||||
) : (
|
</div>
|
||||||
roles.map((role) => (
|
</div>
|
||||||
<option key={role.role_code} value={role.role_code}>
|
|
||||||
{role.role_name}
|
|
||||||
</option>
|
|
||||||
))
|
|
||||||
)
|
|
||||||
) : createRoleOptions.length === 0 ? (
|
|
||||||
<option value="" disabled>
|
|
||||||
加载中…
|
|
||||||
</option>
|
|
||||||
) : (
|
|
||||||
createRoleOptions.map((role) => (
|
|
||||||
<option key={role.role_code} value={role.role_code}>
|
|
||||||
{role.role_name}
|
|
||||||
</option>
|
|
||||||
))
|
|
||||||
)}
|
)}
|
||||||
</select>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
{!editing && (
|
{!editing && (
|
||||||
<CreateUserProjectField
|
<CreateUserProjectField
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import Editor from "@monaco-editor/react";
|
import Editor from "@monaco-editor/react";
|
||||||
|
import { displayScriptPath } from "./scriptPath";
|
||||||
import { Info, Loader2Icon } from "lucide-react";
|
import { Info, Loader2Icon } from "lucide-react";
|
||||||
|
|
||||||
import type { ScriptItem } from "../../services/api";
|
import type { ScriptItem } from "../../services/api";
|
||||||
@@ -114,7 +115,7 @@ export function PythonEditor({
|
|||||||
<i className="size-1.5 rounded-full bg-[#20b77d]" />
|
<i className="size-1.5 rounded-full bg-[#20b77d]" />
|
||||||
Workspace Python Editor
|
Workspace Python Editor
|
||||||
</span>
|
</span>
|
||||||
<span>{script.relative_path}</span>
|
<span>{displayScriptPath(script.relative_path)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative min-h-0 flex-1 overflow-hidden">
|
<div className="relative min-h-0 flex-1 overflow-hidden">
|
||||||
<Editor
|
<Editor
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {type PythonEditorBuffer, useScriptWorkspaceStore} from "./state/scriptWo
|
|||||||
import { useAuth } from "../../context/AuthContext";
|
import { useAuth } from "../../context/AuthContext";
|
||||||
import { getScriptContent } from "../../services/api";
|
import { getScriptContent } from "../../services/api";
|
||||||
import {PythonEditor} from "~/features/platform/PythonEditor";
|
import {PythonEditor} from "~/features/platform/PythonEditor";
|
||||||
|
import { displayScriptPath } from "./scriptPath";
|
||||||
|
|
||||||
function fileIconTone(type: ScriptType | "data") {
|
function fileIconTone(type: ScriptType | "data") {
|
||||||
if (type === "notebook") return "text-[#e15e50] bg-[#fff0ed]";
|
if (type === "notebook") return "text-[#e15e50] bg-[#fff0ed]";
|
||||||
@@ -761,7 +762,7 @@ export function ScriptWorkspace({
|
|||||||
{script.script_name}
|
{script.script_name}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="m-0 font-mono text-[10px] text-[#8997a6]">
|
<p className="m-0 font-mono text-[10px] text-[#8997a6]">
|
||||||
{script.relative_path}
|
{displayScriptPath(script.relative_path)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* Storage metadata keeps workspace files under
|
||||||
|
* `workspace/<owner-user-id>/<logical-path>`. That owner segment is an
|
||||||
|
* implementation detail and should never be presented as part of a script's
|
||||||
|
* user-facing path.
|
||||||
|
*/
|
||||||
|
export function displayScriptPath(relativePath: string): string {
|
||||||
|
const normalized = relativePath.replaceAll("\\", "/");
|
||||||
|
const parts = normalized.split("/");
|
||||||
|
|
||||||
|
if (parts[0] === "workspace" && parts[1] && parts.length > 2) {
|
||||||
|
return parts.slice(2).join("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user