运营后台前端:权限管理(RBAC)+ 系统配置 + 比价记录店/商品搜索 (#36)

一、权限管理(原「管理员」页,重命名为「权限管理」)
- 重建为 4 视图:账号列表 / 新增·编辑人员 / 角色设置(只读权限矩阵,无权限置灰)/ 角色权限勾选矩阵
- 左侧导航按当前角色的可见页过滤(拉 /me 刷新);角色显示中文名 管理员/运营/财务/技术
- 新增人员自动生成一次性密码;编辑复看已确定登录密码(只读),无留存明文的账号不显示密码

二、系统配置
- 首页数据「保存」改为保存即生效
- 首页轮播种子页加 真实/种子/混播 数据源切换

三、比价记录
- 店/商品拆两列 + 各自搜索框;整格换行不截断;搜索命中黄底高亮

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: zzhyyyyy <2685922758@qq.com>
Reviewed-on: #36
Co-authored-by: zhuzihao <zhuzihao@wonderable.ai>
Co-committed-by: zhuzihao <zhuzihao@wonderable.ai>
This commit was merged in pull request #36.
This commit is contained in:
2026-07-05 09:46:24 +08:00
committed by marco
parent 281cfdb0e4
commit b620948a71
6 changed files with 535 additions and 112 deletions
@@ -9,6 +9,7 @@ import {
InputNumber,
Modal,
Popconfirm,
Segmented,
Space,
Spin,
Switch,
@@ -55,6 +56,8 @@ export default function HomeMarqueeSeeds() {
const [seeds, setSeeds] = useState<Seed[]>([]);
const [loading, setLoading] = useState(true);
const [selectedRowKeys, setSelectedRowKeys] = useState<number[]>([]); // 批量操作选中行
const [mode, setMode] = useState<string>('mixed'); // 轮播数据源 mixed/real/seed
const [modeSaving, setModeSaving] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
const [editing, setEditing] = useState<Seed | null>(null);
@@ -80,8 +83,29 @@ export default function HomeMarqueeSeeds() {
};
useEffect(() => {
load();
// 轮播数据源模式(mixed/real/seed):单独拉,与种子列表解耦
api
.get<{ mode: string }>('/admin/api/marquee-seeds/mode')
.then(({ data }) => setMode(data.mode))
.catch(() => {});
}, []);
// 切换数据源:乐观更新 + 失败回滚
const changeMode = async (m: string) => {
const prev = mode;
setMode(m);
setModeSaving(true);
try {
await api.patch('/admin/api/marquee-seeds/mode', { mode: m });
message.success('已切换,客户端下次进首页生效');
} catch (e) {
setMode(prev);
message.error(errMsg(e));
} finally {
setModeSaving(false);
}
};
// ===== 单条 新增 / 编辑 =====
const openAdd = () => {
setEditing(null);
@@ -260,6 +284,22 @@ export default function HomeMarqueeSeeds() {
xxx xx元;
;
</p>
<Space style={{ marginBottom: 12 }} align="center" wrap>
<span style={{ fontSize: 13 }}></span>
<Segmented
value={mode}
disabled={modeSaving}
onChange={(v) => changeMode(v as string)}
options={[
{ label: '混播(默认)', value: 'mixed' },
{ label: '只真实', value: 'real' },
{ label: '只种子', value: 'seed' },
]}
/>
<span style={{ color: '#999', fontSize: 12 }}>
=+;=();=/()
</span>
</Space>
<Space style={{ marginBottom: 12 }} wrap>
<Button type="primary" size="small" onClick={openAdd}>
+7 -10
View File
@@ -8,7 +8,6 @@ import {
Checkbox,
Col,
InputNumber,
Popconfirm,
Radio,
Row,
Select,
@@ -630,16 +629,14 @@ export default function HomeStatsConfig() {
{!loading && (
<div style={{ marginTop: 16 }}>
<Space>
<Button type="primary" loading={saving != null} onClick={() => saveAll(false)}>
{/* 保存即生效:PATCH 带 apply_now,不再等各自「更新时间」的 tick(原「改了 app 不变」的主因)。
自增长模式下保存会顺带推进一档。改小数字若没生效,记得勾对应卡片的「允许数字下降」。 */}
<Button type="primary" loading={saving != null} onClick={() => saveAll(true)}>
()
</Button>
<Popconfirm
title="立即更新会马上把三项都推进一次(自增长各走一档),不等更新时间。确定?"
onConfirm={() => saveAll(true)}
>
<Button loading={saving != null}></Button>
</Popconfirm>
<span style={{ color: '#999', fontSize: 12 }}>,</span>
<span style={{ color: '#999', fontSize: 12 }}>
,,
</span>
</Space>
</div>
)}