Commit b52ff11f authored by David Yang's avatar David Yang

fix: bugs

parent 40f35588
...@@ -27,6 +27,10 @@ function asSaveValue(value, fieldDef) { ...@@ -27,6 +27,10 @@ function asSaveValue(value, fieldDef) {
return value; return value;
} }
function getClearedValue(fieldDef) {
return fieldDef?.multiple ? [] : null;
}
function Pill({ children, tone = 'zinc' }) { function Pill({ children, tone = 'zinc' }) {
const toneClass = const toneClass =
tone === 'rose' tone === 'rose'
...@@ -241,6 +245,12 @@ export default function TailwindEditableField({ ...@@ -241,6 +245,12 @@ export default function TailwindEditableField({
} }
}; };
const clearValue = async () => {
const clearedValue = getClearedValue(fieldDef);
setDraft(clearedValue);
await save(clearedValue);
};
const cancel = () => { const cancel = () => {
setDraft(value); setDraft(value);
setError(''); setError('');
...@@ -259,7 +269,8 @@ export default function TailwindEditableField({ ...@@ -259,7 +269,8 @@ export default function TailwindEditableField({
try { try {
const uploaded = await uploadFile(file); const uploaded = await uploadFile(file);
const fileValue = uploaded?.file || uploaded?.data || uploaded; const fileValue = uploaded?.file || uploaded?.data || uploaded;
const nextValue = fieldDef?.multiple ? [...toArray(value), fileValue] : fileValue; const nextValue = fieldDef?.multiple ? [...toArray(draft), fileValue] : fileValue;
setDraft(nextValue);
await onSave(nextValue); await onSave(nextValue);
setEditing(false); setEditing(false);
} catch (uploadError) { } catch (uploadError) {
...@@ -269,6 +280,25 @@ export default function TailwindEditableField({ ...@@ -269,6 +280,25 @@ export default function TailwindEditableField({
} }
}; };
const handleRemoveFile = async (indexToRemove) => {
setError('');
const nextValue = fieldDef?.multiple
? toArray(draft).filter((_, index) => index !== indexToRemove)
: null;
try {
setDraft(nextValue);
await onSave(nextValue);
if (!fieldDef?.multiple) {
setEditing(false);
}
} catch (saveError) {
setDraft(draft);
setError(saveError?.message || '删除附件失败,请稍后重试。');
}
};
if (!editing) { if (!editing) {
return ( return (
<button <button
...@@ -300,7 +330,10 @@ export default function TailwindEditableField({ ...@@ -300,7 +330,10 @@ export default function TailwindEditableField({
<div className="flex items-center gap-3 rounded-2xl border border-zinc-200 bg-white px-3 py-2"> <div className="flex items-center gap-3 rounded-2xl border border-zinc-200 bg-white px-3 py-2">
<Checkbox checked={Boolean(draft)} onChange={(checked) => setDraft(checked)} color="rose" /> <Checkbox checked={Boolean(draft)} onChange={(checked) => setDraft(checked)} color="rose" />
<span className="text-sm text-zinc-700">{draft ? '是' : '否'}</span> <span className="text-sm text-zinc-700">{draft ? '是' : '否'}</span>
<button type="button" onClick={() => save(Boolean(draft))} className="ml-auto text-sm font-medium text-rose-700"> <button type="button" onClick={clearValue} className="ml-auto text-sm font-medium text-rose-600">
清空
</button>
<button type="button" onClick={() => save(Boolean(draft))} className="text-sm font-medium text-rose-700">
保存 保存
</button> </button>
<button type="button" onClick={cancel} className="text-sm text-zinc-500"> <button type="button" onClick={cancel} className="text-sm text-zinc-500">
...@@ -334,6 +367,7 @@ export default function TailwindEditableField({ ...@@ -334,6 +367,7 @@ export default function TailwindEditableField({
))} ))}
</Select> </Select>
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<button type="button" onClick={clearValue} className="text-sm font-medium text-rose-600">清空</button>
<button type="button" onClick={cancel} className="text-sm text-zinc-500">取消</button> <button type="button" onClick={cancel} className="text-sm text-zinc-500">取消</button>
<button type="button" onClick={() => save()} className="text-sm font-medium text-rose-700">保存</button> <button type="button" onClick={() => save()} className="text-sm font-medium text-rose-700">保存</button>
</div> </div>
...@@ -347,6 +381,7 @@ export default function TailwindEditableField({ ...@@ -347,6 +381,7 @@ export default function TailwindEditableField({
<div className="space-y-3"> <div className="space-y-3">
<UserSelector value={draft} onChange={setDraft} multiple={fieldDef?.multiple} /> <UserSelector value={draft} onChange={setDraft} multiple={fieldDef?.multiple} />
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<button type="button" onClick={clearValue} className="text-sm font-medium text-rose-600">清空</button>
<button type="button" onClick={cancel} className="text-sm text-zinc-500">取消</button> <button type="button" onClick={cancel} className="text-sm text-zinc-500">取消</button>
<button type="button" onClick={() => save()} className="text-sm font-medium text-rose-700">保存</button> <button type="button" onClick={() => save()} className="text-sm font-medium text-rose-700">保存</button>
</div> </div>
...@@ -364,6 +399,7 @@ export default function TailwindEditableField({ ...@@ -364,6 +399,7 @@ export default function TailwindEditableField({
onChange={setDraft} onChange={setDraft}
/> />
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<button type="button" onClick={clearValue} className="text-sm font-medium text-rose-600">清空</button>
<button type="button" onClick={cancel} className="text-sm text-zinc-500">取消</button> <button type="button" onClick={cancel} className="text-sm text-zinc-500">取消</button>
<button type="button" onClick={() => save()} className="text-sm font-medium text-rose-700">保存</button> <button type="button" onClick={() => save()} className="text-sm font-medium text-rose-700">保存</button>
</div> </div>
...@@ -375,10 +411,47 @@ export default function TailwindEditableField({ ...@@ -375,10 +411,47 @@ export default function TailwindEditableField({
if (fieldType === 'file') { if (fieldType === 'file') {
return ( return (
<div className="space-y-3 rounded-2xl border border-zinc-200 bg-white p-3"> <div className="space-y-3 rounded-2xl border border-zinc-200 bg-white p-3">
<FieldValue value={value} fieldDef={fieldDef} /> <div className="space-y-2">
{toArray(draft).length === 0 ? (
<span className="text-zinc-400">未上传</span>
) : (
toArray(draft).map((file, index) => {
const label = file?.name || file?.title || file?.url || `附件 ${index + 1}`;
return (
<div
key={`${label}-${index}`}
className="flex items-center justify-between gap-3 rounded-2xl border border-zinc-100 bg-zinc-50 px-3 py-2"
>
<div className="min-w-0 flex-1">
{file?.url ? (
<a
href={file.url}
target="_blank"
rel="noreferrer"
className="text-sm font-medium text-sky-700 transition hover:text-sky-800"
>
{label}
</a>
) : (
<div className="text-sm font-medium text-zinc-700">{label}</div>
)}
</div>
<button
type="button"
onClick={() => handleRemoveFile(index)}
className="shrink-0 text-sm font-medium text-rose-600"
>
删除
</button>
</div>
);
})
)}
</div>
<input type="file" onChange={handleFileUpload} disabled={uploading} className="block w-full text-sm text-zinc-600" /> <input type="file" onChange={handleFileUpload} disabled={uploading} className="block w-full text-sm text-zinc-600" />
{error ? <div className="text-sm text-rose-600">{error}</div> : null} {error ? <div className="text-sm text-rose-600">{error}</div> : null}
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<button type="button" onClick={clearValue} className="text-sm font-medium text-rose-600">清空</button>
<button type="button" onClick={cancel} className="text-sm text-zinc-500">取消</button> <button type="button" onClick={cancel} className="text-sm text-zinc-500">取消</button>
<span className="text-sm text-zinc-400">{uploading ? '上传中...' : '选择文件后自动保存'}</span> <span className="text-sm text-zinc-400">{uploading ? '上传中...' : '选择文件后自动保存'}</span>
</div> </div>
...@@ -393,6 +466,7 @@ export default function TailwindEditableField({ ...@@ -393,6 +466,7 @@ export default function TailwindEditableField({
<Textarea rows={8} value={jsonValue} onChange={(event) => setDraft(event.target.value)} resizable /> <Textarea rows={8} value={jsonValue} onChange={(event) => setDraft(event.target.value)} resizable />
{error ? <div className="text-sm text-rose-600">{error}</div> : null} {error ? <div className="text-sm text-rose-600">{error}</div> : null}
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<button type="button" onClick={clearValue} className="text-sm font-medium text-rose-600">清空</button>
<button type="button" onClick={cancel} className="text-sm text-zinc-500">取消</button> <button type="button" onClick={cancel} className="text-sm text-zinc-500">取消</button>
<button <button
type="button" type="button"
...@@ -411,6 +485,7 @@ export default function TailwindEditableField({ ...@@ -411,6 +485,7 @@ export default function TailwindEditableField({
<div className="space-y-3"> <div className="space-y-3">
<Textarea rows={5} value={draft || ''} onChange={(event) => setDraft(event.target.value)} autoFocus /> <Textarea rows={5} value={draft || ''} onChange={(event) => setDraft(event.target.value)} autoFocus />
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<button type="button" onClick={clearValue} className="text-sm font-medium text-rose-600">清空</button>
<button type="button" onClick={cancel} className="text-sm text-zinc-500">取消</button> <button type="button" onClick={cancel} className="text-sm text-zinc-500">取消</button>
<button type="button" onClick={() => save()} className="text-sm font-medium text-rose-700">保存</button> <button type="button" onClick={() => save()} className="text-sm font-medium text-rose-700">保存</button>
</div> </div>
...@@ -433,6 +508,7 @@ export default function TailwindEditableField({ ...@@ -433,6 +508,7 @@ export default function TailwindEditableField({
autoFocus autoFocus
/> />
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<button type="button" onClick={clearValue} className="text-sm font-medium text-rose-600">清空</button>
<button type="button" onClick={cancel} className="text-sm text-zinc-500">取消</button> <button type="button" onClick={cancel} className="text-sm text-zinc-500">取消</button>
<button type="button" onClick={() => save()} className="text-sm font-medium text-rose-700">保存</button> <button type="button" onClick={() => save()} className="text-sm font-medium text-rose-700">保存</button>
</div> </div>
......
...@@ -63,6 +63,7 @@ const RecordDetailDrawer = (props, ref) => { ...@@ -63,6 +63,7 @@ const RecordDetailDrawer = (props, ref) => {
onDataChange, onDataChange,
readonly = false, readonly = false,
filterView = [], filterView = [],
preferredViewName,
} = props; } = props;
const [refreshKey, setRefreshKey] = useState(() => Date.now()); const [refreshKey, setRefreshKey] = useState(() => Date.now());
...@@ -112,11 +113,21 @@ const RecordDetailDrawer = (props, ref) => { ...@@ -112,11 +113,21 @@ const RecordDetailDrawer = (props, ref) => {
return; return;
} }
if (preferredViewName) {
const matchedView = detailViews.find(
(view) => view.name === preferredViewName || view.title === preferredViewName,
);
if (matchedView && matchedView.name !== activeTabKey) {
setActiveTabKey(matchedView.name);
return;
}
}
const currentTab = detailViews.find((view) => view.name === activeTabKey); const currentTab = detailViews.find((view) => view.name === activeTabKey);
if (!currentTab) { if (!currentTab) {
setActiveTabKey(detailViews[0].name); setActiveTabKey(detailViews[0].name);
} }
}, [detailViews, activeTabKey]); }, [detailViews, activeTabKey, preferredViewName]);
useImperativeHandle( useImperativeHandle(
ref, ref,
......
import xyLogo from '@/assets/xy-logo.png';
import xyLogoSmall from '@/assets/xy-logo_small.png'; import xyLogoSmall from '@/assets/xy-logo_small.png';
import { UserContext } from '@/wrapper/Auth'; import { UserContext } from '@/wrapper/Auth';
import { import {
...@@ -244,16 +243,13 @@ export default function ProblemRectificationLayout() { ...@@ -244,16 +243,13 @@ export default function ProblemRectificationLayout() {
sidebarCollapsed ? 'justify-center' : 'gap-3 px-1', sidebarCollapsed ? 'justify-center' : 'gap-3 px-1',
)} )}
> >
{sidebarCollapsed ? (
<img <img
src={sidebarCollapsed ? xyLogoSmall : xyLogo} src={xyLogoSmall}
alt="XY Logo" alt="XY Logo"
className={clsx( className="h-8 w-auto max-w-8 shrink-0 object-contain"
'shrink-0 object-contain',
sidebarCollapsed
? 'h-8 w-auto max-w-8'
: 'h-8 w-auto max-w-[140px]',
)}
/> />
) : null}
{!sidebarCollapsed ? ( {!sidebarCollapsed ? (
<div className="min-w-0"> <div className="min-w-0">
<h1 className="truncate text-sm font-semibold tracking-[0.02em] text-white/95"> <h1 className="truncate text-sm font-semibold tracking-[0.02em] text-white/95">
...@@ -327,12 +323,7 @@ export default function ProblemRectificationLayout() { ...@@ -327,12 +323,7 @@ export default function ProblemRectificationLayout() {
close={() => setIsMobileSidebarOpen(false)} close={() => setIsMobileSidebarOpen(false)}
> >
<div className="px-5 pt-6"> <div className="px-5 pt-6">
<div className="flex items-center gap-2"> <div>
<img
src={xyLogo}
alt="XY Logo"
className="h-8 w-auto max-w-[140px] shrink-0 object-contain"
/>
<div> <div>
<h1 className="text-base font-semibold tracking-[0.02em] text-white"> <h1 className="text-base font-semibold tracking-[0.02em] text-white">
{APP_NAME} {APP_NAME}
......
...@@ -51,6 +51,19 @@ ...@@ -51,6 +51,19 @@
- 新增、编辑、查看详情优先采用抽屉或弹窗,不走重型嵌套表格交互 - 新增、编辑、查看详情优先采用抽屉或弹窗,不走重型嵌套表格交互
- 批量操作放在顶部工具栏,不再依赖右键菜单作为唯一入口 - 批量操作放在顶部工具栏,不再依赖右键菜单作为唯一入口
### 3.3 视图配置驱动约定
问题整改主列表与详情抽屉统一以 `getSubjectView(subjectName)` 返回的视图配置驱动,不再在页面内写死部门标签和字段顺序:
- `type = "grid"` 的视图用于主列表顶部视图切换,替代原先写死的 tab
- `settings.columns` 决定列表列顺序、宽度和排序优先级
- `settings.restrictions` 与 `settings.filters.conditions` 共同决定该视图下可见的数据范围
- `settings.viewAuth` 决定当前用户是否能看到该视图
- `type = "detail"` 的视图用于详情抽屉字段编排,按 `columns` 顺序渲染字段
- `settings.auth` 与 `disableColumns` 用于控制详情抽屉内字段是否允许编辑
因此前端新增字段展示、部门视图调整、列顺序变化时,应优先改 Subject View 配置,而不是直接修改页面常量。
### 3.1 目标页面拆分 ### 3.1 目标页面拆分
为便于当前项目直接重构,建议把模块拆成以下 4 个页面入口: 为便于当前项目直接重构,建议把模块拆成以下 4 个页面入口:
...@@ -179,6 +192,13 @@ ...@@ -179,6 +192,13 @@
- 表格内分组拖拽 - 表格内分组拖拽
- 全屏容器能力 - 全屏容器能力
替代实现约定:
- 列表页保留表格浏览、搜索、筛选、批量选择
- 字段编辑统一进入右侧详情抽屉
- 长文本、附件、状态类字段在详情抽屉中直接维护
- 详情抽屉内字段顺序与显示权限跟随 `detail view` 配置
--- ---
## 6. 流程角色 ## 6. 流程角色
...@@ -624,6 +644,7 @@ const result = await queryUsers(0, 20, { ...@@ -624,6 +644,7 @@ const result = await queryUsers(0, 20, {
布局层负责: 布局层负责:
- 左侧菜单 - 左侧菜单
- 桌面展开态仅展示模块名称,去掉大 Logo;仅在桌面折叠态展示小 Logo
- 顶部用户区 - 顶部用户区
- 内容容器 - 内容容器
- 移动端侧栏开关 - 移动端侧栏开关
......
...@@ -7,6 +7,7 @@ import { ...@@ -7,6 +7,7 @@ import {
import { DEPARTMENT_GROUPS } from './problemRectificationDashboardService'; import { DEPARTMENT_GROUPS } from './problemRectificationDashboardService';
import { import {
buildViewColumns, buildViewColumns,
checkViewCondition,
emptyValueForField, emptyValueForField,
formatFieldValue, formatFieldValue,
getFieldNameFromPath, getFieldNameFromPath,
...@@ -162,6 +163,18 @@ export function buildProblemRectificationConfiguredListView(records, filters, gr ...@@ -162,6 +163,18 @@ export function buildProblemRectificationConfiguredListView(records, filters, gr
return false; return false;
} }
if (
!checkViewCondition(
{
relationship: gridView?.settings?.filters?.relationship || 'all',
rules: gridView?.settings?.filters?.conditions || [],
},
rawRecord,
)
) {
return false;
}
if (nextProgress && record.progress !== nextProgress) { if (nextProgress && record.progress !== nextProgress) {
return false; return false;
} }
...@@ -200,6 +213,18 @@ export function buildProblemRectificationConfiguredListView(records, filters, gr ...@@ -200,6 +213,18 @@ export function buildProblemRectificationConfiguredListView(records, filters, gr
}; };
} }
export function buildGridViewCountMap(records, filters, views, subject) {
return views.reduce((counts, view) => {
counts[view.name] = buildProblemRectificationConfiguredListView(
records,
filters,
view,
subject,
).filteredRecords.length;
return counts;
}, {});
}
function matchesDepartmentGroup(record, groupKey) { function matchesDepartmentGroup(record, groupKey) {
if (!groupKey || groupKey === 'all') { if (!groupKey || groupKey === 'all') {
return true; return true;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment