Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
C
center-book
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
EzijingWeb
center-book
Commits
195f403c
提交
195f403c
authored
7月 20, 2026
作者:
王鹏飞
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
refactor(admin): migrate settings feedback module
上级
ff815a97
显示空白字符变更
内嵌
并排
正在显示
9 个修改的文件
包含
443 行增加
和
93 行删除
+443
-93
task-5-report.md
.superpowers/sdd/task-5-report.md
+61
-0
api.ts
apps/admin/src/modules/settings/feedback/api.ts
+36
-0
query.ts
apps/admin/src/modules/settings/feedback/query.ts
+48
-0
routes.tsx
apps/admin/src/modules/settings/feedback/routes.tsx
+10
-0
useFeedbackList.ts
apps/admin/src/modules/settings/feedback/useFeedbackList.ts
+156
-0
FeedbackListView.tsx
.../src/modules/settings/feedback/views/FeedbackListView.tsx
+130
-0
routes.tsx
apps/admin/src/modules/settings/routes.tsx
+2
-2
api.js
apps/admin/src/pages/setting/feedback/api.js
+0
-6
index.jsx
apps/admin/src/pages/setting/feedback/index.jsx
+0
-85
没有找到文件。
.superpowers/sdd/task-5-report.md
0 → 100644
浏览文件 @
195f403c
# Task 5 report: settings/feedback migration
## Status
Completed the
`settings/feedback`
migration from the legacy page directory to
`apps/admin/src/modules/settings/feedback`
. The
`/setting/feedback`
route now
lazy-loads the module-owned route and view. The legacy feedback page and API
files were removed only after the module build and regression checks passed.
## Delivered structure
-
`api.ts`
: retains the existing axios client, POST endpoint, and object body;
awaits the business envelope before normalizing
`data.list`
/
`data.total`
to
`{ rows, total }`
.
-
`query.ts`
: maps
`pageSize`
to
`page_size`
, maps the selected time range to
second-based
`start_time`
/
`end_time`
, removes the UI-only
`time`
field, and
preserves meaningful falsy values rather than applying generic cleanup.
-
`useFeedbackList.ts`
: owns draft/applied filters, controlled pagination,
rows, total, loading, error, retry, and request sequencing.
-
`views/FeedbackListView.tsx`
: preserves the three filters and the five legacy
columns in their original order, including the 400px two-line feedback cell
with full text in a Popover. The page has no permissions, toolbar, row
selection, or row actions, matching the legacy page.
-
`routes.tsx`
: owns the feedback leaf route and fallback without changing the
public URL.
-
`modules/settings/routes.tsx`
: delegates
`feedback/*`
to the new module.
## Preserved behavior
-
API:
`POST /api/system/proposal/getList`
with an object body.
-
Initial pagination: page 1, page size 10; allowed sizes 10/20/30/40.
-
Search and reset return to page 1 and issue one explicit request.
-
Pagination uses applied filters; refresh keeps the current page and applies
the current filter draft, matching the old form-backed refresh behavior.
-
Clearing a filter explicitly reloads the current page, matching the old
`onValuesChange`
behavior.
-
Failures end loading, retain existing rows when present, surface an error to
AppList, and retry the last request.
-
A request sequence prevents an older response from overwriting newer data.
## Verification
-
`pnpm test`
: passed, 11/11 global Node tests.
-
`pnpm test:unit --run`
: passed, 10/10 smoke and controlled AppList tests.
-
Feedback development guards before their removal per the updated task scope:
passed, 14/14 query/API/Hook tests. These files were not committed.
-
`pnpm exec oxlint src/modules/settings/feedback --deny-warnings`
: passed.
-
`pnpm exec oxfmt --check src/modules/settings/feedback`
: passed.
-
`pnpm build`
: passed; existing formula-editor and large-chunk warnings remain.
-
`pnpm exec tsc --noEmit`
: feedback introduced no diagnostics. The command
remains nonzero for existing/shared concurrent issues in
`App.tsx`
,
`RouteErrorBoundary.tsx`
, and the shared AppList LESS side-effect import.
## Concerns
1.
Per the updated task scope, no feedback API/query/Hook test files are part of
this commit. The development-only guards were removed before staging.
2.
Legacy, unused route metadata files still contain imports of the removed old
page path. The active
`src/app/routes.tsx`
path uses the new domain route and
production build succeeds; updating deprecated route metadata was outside
this task's authorized scope.
apps/admin/src/modules/settings/feedback/api.ts
0 → 100644
浏览文件 @
195f403c
import
axios
from
"@/utils/axios"
;
import
type
{
FeedbackQuery
}
from
"./query"
;
export
interface
FeedbackRow
{
id
:
string
|
number
;
name
?:
string
;
phone
?:
string
;
create_time
?:
string
;
proposal
?:
string
;
}
export
interface
FeedbackListResult
{
rows
:
FeedbackRow
[];
total
:
number
;
}
interface
FeedbackListEnvelope
{
data
?:
{
list
?:
FeedbackRow
[];
total
?:
number
;
};
}
export
type
FeedbackListRequest
=
(
params
:
FeedbackQuery
)
=>
Promise
<
FeedbackListResult
>
;
// 保留旧 axios 客户端和业务 envelope,await 后才做模块内归一化。
export
async
function
getFeedbackList
(
params
:
FeedbackQuery
):
Promise
<
FeedbackListResult
>
{
const
response
=
(
await
axios
.
post
(
"/api/system/proposal/getList"
,
params
,
))
as
unknown
as
FeedbackListEnvelope
;
const
{
list
=
[],
total
=
0
}
=
response
.
data
??
{};
return
{
rows
:
list
,
total
};
}
apps/admin/src/modules/settings/feedback/query.ts
0 → 100644
浏览文件 @
195f403c
import
type
{
Dayjs
}
from
"dayjs"
;
export
type
FeedbackFilterValue
=
string
|
number
|
boolean
|
null
;
export
interface
FeedbackFilters
{
name
?:
FeedbackFilterValue
;
phone
?:
FeedbackFilterValue
;
time
?:
[
Dayjs
|
null
,
Dayjs
|
null
]
|
null
;
}
export
interface
FeedbackQuery
{
page
:
number
;
page_size
:
number
;
name
?:
FeedbackFilterValue
;
phone
?:
FeedbackFilterValue
;
start_time
?:
number
;
end_time
?:
number
;
}
interface
BuildFeedbackQueryInput
{
page
:
number
;
pageSize
:
number
;
filters
:
FeedbackFilters
;
}
export
function
buildFeedbackQuery
({
page
,
pageSize
,
filters
,
}:
BuildFeedbackQueryInput
):
FeedbackQuery
{
const
query
:
FeedbackQuery
=
{
page
,
page_size
:
pageSize
,
};
if
(
filters
.
name
!==
undefined
)
{
query
.
name
=
filters
.
name
;
}
if
(
filters
.
phone
!==
undefined
)
{
query
.
phone
=
filters
.
phone
;
}
if
(
filters
.
time
?.[
0
]
&&
filters
.
time
[
1
])
{
query
.
start_time
=
filters
.
time
[
0
].
valueOf
()
/
1000
;
query
.
end_time
=
filters
.
time
[
1
].
valueOf
()
/
1000
;
}
return
query
;
}
apps/admin/src/modules/settings/feedback/routes.tsx
0 → 100644
浏览文件 @
195f403c
import
{
Navigate
,
useRoutes
}
from
"react-router"
;
import
FeedbackListView
from
"./views/FeedbackListView"
;
export
default
function
FeedbackRoutes
()
{
return
useRoutes
([
{
index
:
true
,
Component
:
FeedbackListView
},
{
path
:
"*"
,
element
:
<
Navigate
to=
"/setting/feedback"
/>
},
]);
}
apps/admin/src/modules/settings/feedback/useFeedbackList.ts
0 → 100644
浏览文件 @
195f403c
import
{
useCallback
,
useEffect
,
useRef
,
useState
}
from
"react"
;
import
{
getFeedbackList
,
type
FeedbackListRequest
,
type
FeedbackListResult
}
from
"./api"
;
import
{
buildFeedbackQuery
,
type
FeedbackFilters
,
type
FeedbackQuery
}
from
"./query"
;
const
INITIAL_PAGE
=
1
;
const
INITIAL_PAGE_SIZE
=
10
;
interface
UseFeedbackListOptions
{
request
?:
FeedbackListRequest
;
}
function
toError
(
reason
:
unknown
)
{
return
reason
instanceof
Error
?
reason
:
new
Error
(
"请求失败"
);
}
function
wasLegacyClear
(
previous
:
FeedbackFilters
,
next
:
FeedbackFilters
)
{
const
keys
=
new
Set
([...
Object
.
keys
(
previous
),
...
Object
.
keys
(
next
)])
as
Set
<
keyof
FeedbackFilters
>
;
return
[...
keys
].
some
((
key
)
=>
previous
[
key
]
!==
next
[
key
]
&&
!
next
[
key
]);
}
export
function
useFeedbackList
({
request
=
getFeedbackList
}:
UseFeedbackListOptions
=
{})
{
const
[
filters
,
setFilterState
]
=
useState
<
FeedbackFilters
>
({});
const
[
appliedFilters
,
setAppliedFilters
]
=
useState
<
FeedbackFilters
>
({});
const
[
page
,
setPage
]
=
useState
(
INITIAL_PAGE
);
const
[
pageSize
,
setPageSize
]
=
useState
(
INITIAL_PAGE_SIZE
);
const
[
rows
,
setRows
]
=
useState
<
FeedbackListResult
[
"rows"
]
>
([]);
const
[
total
,
setTotal
]
=
useState
(
0
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
error
,
setError
]
=
useState
<
Error
|
null
>
(
null
);
const
requestRef
=
useRef
(
request
);
const
requestIdRef
=
useRef
(
0
);
const
lastQueryRef
=
useRef
<
FeedbackQuery
>
(
buildFeedbackQuery
({
page
:
INITIAL_PAGE
,
pageSize
:
INITIAL_PAGE_SIZE
,
filters
:
{}
}),
);
useEffect
(()
=>
{
requestRef
.
current
=
request
;
},
[
request
]);
const
load
=
useCallback
(
async
(
query
:
FeedbackQuery
)
=>
{
const
requestId
=
++
requestIdRef
.
current
;
lastQueryRef
.
current
=
query
;
setLoading
(
true
);
setError
(
null
);
try
{
const
result
=
await
requestRef
.
current
(
query
);
if
(
requestId
===
requestIdRef
.
current
)
{
setRows
(
result
.
rows
);
setTotal
(
result
.
total
);
}
}
catch
(
reason
)
{
if
(
requestId
===
requestIdRef
.
current
)
{
setError
(
toError
(
reason
));
}
}
finally
{
if
(
requestId
===
requestIdRef
.
current
)
{
setLoading
(
false
);
}
}
},
[]);
useEffect
(()
=>
{
void
load
(
lastQueryRef
.
current
);
return
()
=>
{
requestIdRef
.
current
+=
1
;
};
},
[
load
]);
const
loadFor
=
useCallback
(
(
nextPage
:
number
,
nextPageSize
:
number
,
nextFilters
:
FeedbackFilters
)
=>
load
(
buildFeedbackQuery
({
page
:
nextPage
,
pageSize
:
nextPageSize
,
filters
:
nextFilters
,
}),
),
[
load
],
);
const
setFilters
=
useCallback
(
(
nextFilters
:
FeedbackFilters
)
=>
{
const
shouldReload
=
wasLegacyClear
(
filters
,
nextFilters
);
setFilterState
(
nextFilters
);
if
(
shouldReload
)
{
setAppliedFilters
(
nextFilters
);
void
loadFor
(
page
,
pageSize
,
nextFilters
);
}
},
[
filters
,
loadFor
,
page
,
pageSize
],
);
const
search
=
useCallback
(()
=>
{
setAppliedFilters
(
filters
);
setPage
(
INITIAL_PAGE
);
void
loadFor
(
INITIAL_PAGE
,
pageSize
,
filters
);
},
[
filters
,
loadFor
,
pageSize
]);
const
reset
=
useCallback
(()
=>
{
const
emptyFilters
:
FeedbackFilters
=
{};
setFilterState
(
emptyFilters
);
setAppliedFilters
(
emptyFilters
);
setPage
(
INITIAL_PAGE
);
void
loadFor
(
INITIAL_PAGE
,
pageSize
,
emptyFilters
);
},
[
loadFor
,
pageSize
]);
const
refresh
=
useCallback
(()
=>
{
setAppliedFilters
(
filters
);
void
loadFor
(
page
,
pageSize
,
filters
);
},
[
filters
,
loadFor
,
page
,
pageSize
]);
const
changePage
=
useCallback
(
(
nextPage
:
number
)
=>
{
setPage
(
nextPage
);
void
loadFor
(
nextPage
,
pageSize
,
appliedFilters
);
},
[
appliedFilters
,
loadFor
,
pageSize
],
);
const
changePageSize
=
useCallback
(
(
nextPageSize
:
number
)
=>
{
setPage
(
INITIAL_PAGE
);
setPageSize
(
nextPageSize
);
void
loadFor
(
INITIAL_PAGE
,
nextPageSize
,
appliedFilters
);
},
[
appliedFilters
,
loadFor
],
);
const
retry
=
useCallback
(()
=>
{
void
load
(
lastQueryRef
.
current
);
},
[
load
]);
return
{
filters
,
setFilters
,
page
,
pageSize
,
rows
,
total
,
loading
,
error
,
search
,
reset
,
refresh
,
changePage
,
changePageSize
,
retry
,
};
}
apps/admin/src/modules/settings/feedback/views/FeedbackListView.tsx
0 → 100644
浏览文件 @
195f403c
import
{
DatePicker
,
Input
,
Popover
}
from
"antd"
;
import
type
{
ColumnsType
}
from
"antd/es/table"
;
import
{
AppList
,
type
AppListFilterItem
}
from
"@/shared/components/AppList"
;
import
type
{
FeedbackRow
}
from
"../api"
;
import
type
{
FeedbackFilterValue
,
FeedbackFilters
}
from
"../query"
;
import
{
useFeedbackList
}
from
"../useFeedbackList"
;
const
{
RangePicker
}
=
DatePicker
;
const
columns
:
ColumnsType
<
FeedbackRow
>
=
[
{
title
:
"ID"
,
key
:
"id"
,
dataIndex
:
"id"
,
},
{
title
:
"用户名称"
,
key
:
"name"
,
dataIndex
:
"name"
,
align
:
"center"
,
},
{
title
:
"手机号"
,
key
:
"phone"
,
dataIndex
:
"phone"
,
align
:
"center"
,
},
{
title
:
"反馈时间"
,
key
:
"create_time"
,
dataIndex
:
"create_time"
,
align
:
"center"
,
},
{
title
:
"反馈内容"
,
key
:
"proposal"
,
dataIndex
:
"proposal"
,
align
:
"center"
,
width
:
400
,
render
:
(
text
:
string
)
=>
(
<
Popover
content=
{
text
}
overlayStyle=
{
{
maxWidth
:
"400px"
}
}
>
<
div
style=
{
{
display
:
"-webkit-box"
,
WebkitLineClamp
:
"2"
,
WebkitBoxOrient
:
"vertical"
,
overflow
:
"hidden"
,
textOverflow
:
"ellipsis"
,
textAlign
:
"center"
,
}
}
>
{
text
}
</
div
>
</
Popover
>
),
},
];
function
inputValue
(
value
:
FeedbackFilterValue
|
undefined
)
{
return
typeof
value
===
"string"
||
typeof
value
===
"number"
?
value
:
""
;
}
const
filterItems
:
AppListFilterItem
<
FeedbackFilters
>
[]
=
[
{
label
:
"用户名称"
,
name
:
"name"
,
render
:
(
field
)
=>
(
<
Input
placeholder=
"请输入用户名称"
allowClear
value=
{
inputValue
(
field
.
value
)
}
onChange=
{
(
event
)
=>
field
.
onChange
(
event
.
target
.
value
)
}
/>
),
},
{
label
:
"手机号"
,
name
:
"phone"
,
render
:
(
field
)
=>
(
<
Input
placeholder=
"请输入手机号"
allowClear
value=
{
inputValue
(
field
.
value
)
}
onChange=
{
(
event
)
=>
field
.
onChange
(
event
.
target
.
value
)
}
/>
),
},
{
label
:
"反馈时间"
,
name
:
"time"
,
render
:
(
field
)
=>
(
<
RangePicker
allowClear
value=
{
field
.
value
}
onChange=
{
(
value
)
=>
field
.
onChange
(
value
)
}
/>
),
},
];
export
default
function
FeedbackListView
()
{
const
list
=
useFeedbackList
();
return
(
<
AppList
<
FeedbackRow
,
FeedbackFilters
>
rowKey="id"
columns=
{
columns
}
data=
{
{
rows
:
list
.
rows
,
total
:
list
.
total
,
loading
:
list
.
loading
,
error
:
list
.
error
,
}
}
filters=
{
{
value
:
list
.
filters
,
items
:
filterItems
,
onChange
:
list
.
setFilters
,
onSearch
:
list
.
search
,
onReset
:
list
.
reset
,
onRefresh
:
list
.
refresh
,
}
}
pagination=
{
{
page
:
list
.
page
,
pageSize
:
list
.
pageSize
,
pageSizeOptions
:
[
10
,
20
,
30
,
40
],
onChange
:
list
.
changePage
,
onPageSizeChange
:
list
.
changePageSize
,
}
}
onRetry=
{
list
.
retry
}
/
>
);
}
apps/admin/src/modules/settings/routes.tsx
浏览文件 @
195f403c
...
@@ -14,7 +14,7 @@ const PointManagement = lazy(() => import("@/pages/setting/point-management/inde
...
@@ -14,7 +14,7 @@ const PointManagement = lazy(() => import("@/pages/setting/point-management/inde
const
AppVersion
=
lazy
(()
=>
import
(
"@/pages/setting/app-version/index"
));
const
AppVersion
=
lazy
(()
=>
import
(
"@/pages/setting/app-version/index"
));
const
Coupon
=
lazy
(()
=>
import
(
"@/pages/setting/coupon/index"
));
const
Coupon
=
lazy
(()
=>
import
(
"@/pages/setting/coupon/index"
));
const
Sensitive
=
lazy
(()
=>
import
(
"@/pages/setting/sensitive/index"
));
const
Sensitive
=
lazy
(()
=>
import
(
"@/pages/setting/sensitive/index"
));
const
Feedback
=
lazy
(()
=>
import
(
"@/pages/setting/feedback/index
"
));
const
Feedback
Routes
=
lazy
(()
=>
import
(
"./feedback/routes
"
));
export
default
function
SettingsRoutes
()
{
export
default
function
SettingsRoutes
()
{
const
routes
=
useRoutes
([
const
routes
=
useRoutes
([
...
@@ -37,7 +37,7 @@ export default function SettingsRoutes() {
...
@@ -37,7 +37,7 @@ export default function SettingsRoutes() {
{
path
:
"app-version"
,
Component
:
AppVersion
},
{
path
:
"app-version"
,
Component
:
AppVersion
},
{
path
:
"coupon"
,
Component
:
Coupon
},
{
path
:
"coupon"
,
Component
:
Coupon
},
{
path
:
"sensitive"
,
Component
:
Sensitive
},
{
path
:
"sensitive"
,
Component
:
Sensitive
},
{
path
:
"feedback
"
,
Component
:
Feedback
},
{
path
:
"feedback
/*"
,
Component
:
FeedbackRoutes
},
],
],
},
},
{
path
:
"*"
,
element
:
<
Navigate
to=
"/"
/>
},
{
path
:
"*"
,
element
:
<
Navigate
to=
"/"
/>
},
...
...
apps/admin/src/pages/setting/feedback/api.js
deleted
100644 → 0
浏览文件 @
ff815a97
import
axios
from
"@/utils/axios"
;
// 获取意见反馈列表
export
function
getList
(
data
)
{
return
axios
.
post
(
"/api/system/proposal/getList"
,
data
);
}
apps/admin/src/pages/setting/feedback/index.jsx
deleted
100644 → 0
浏览文件 @
ff815a97
import
{
Input
,
DatePicker
,
Popover
}
from
"antd"
;
import
AppList
from
"@/components/list"
;
import
{
getList
}
from
"./api"
;
const
{
RangePicker
}
=
DatePicker
;
export
default
function
FeedBack
()
{
const
remote
=
{
request
:
getList
,
beforeRequest
(
params
)
{
const
{
time
,
...
rest
}
=
params
;
if
(
time
)
{
rest
.
start_time
=
new
Date
(
time
[
0
]).
getTime
()
/
1000
;
rest
.
end_time
=
new
Date
(
time
[
1
]).
getTime
()
/
1000
;
}
return
rest
;
},
};
const
filters
=
[
{
label
:
"用户名称"
,
name
:
"name"
,
element
:
<
Input
placeholder=
"请输入用户名称"
allowClear
/>,
},
{
label
:
"手机号"
,
name
:
"phone"
,
element
:
<
Input
placeholder=
"请输入手机号"
allowClear
/>,
},
{
label
:
"反馈时间"
,
name
:
"time"
,
element
:
<
RangePicker
allowClear
/>,
},
];
const
columns
=
[
{
title
:
"ID"
,
key
:
"id"
,
dataIndex
:
"id"
,
},
{
title
:
"用户名称"
,
key
:
"name"
,
dataIndex
:
"name"
,
align
:
"center"
,
},
{
title
:
"手机号"
,
key
:
"phone"
,
dataIndex
:
"phone"
,
align
:
"center"
,
},
{
title
:
"反馈时间"
,
key
:
"create_time"
,
dataIndex
:
"create_time"
,
align
:
"center"
,
},
{
title
:
"反馈内容"
,
key
:
"proposal"
,
dataIndex
:
"proposal"
,
align
:
"center"
,
width
:
400
,
render
:
(
text
)
=>
(
<
Popover
content=
{
text
}
overlayStyle=
{
{
maxWidth
:
"400px"
}
}
>
<
div
style=
{
{
display
:
"-webkit-box"
,
WebkitLineClamp
:
"2"
,
WebkitBoxOrient
:
"vertical"
,
overflow
:
"hidden"
,
textOverflow
:
"ellipsis"
,
textAlign
:
"center"
,
}
}
>
{
text
}
</
div
>
</
Popover
>
),
},
];
return
<
AppList
{
...
{
remote
,
filters
,
columns
}}
></
AppList
>;
}
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论