头部统一 - 新增 PageHeader 组件: 高度 23px (原 NavBar 45px 的一半), 标题 12px 主色加粗, 背景取页面底色 - 打卡 / 宏观 / 监控 三页各自引用, 互不影响; 宏观与监控补齐「宏观」「监控」标题 - 「我」页移除 NavBar 空白条, 蓝色头像区直接贴顶 (不引用 PageHeader, 与其余三页解耦) - 清理失效样式 .me-navbar 清单模板分类 - 分段由 2 项扩为 3 项: 进行中 / 已归档 / 模板; 右上角按钮随分段切换 - 模板独立增删改查: 新增 GET /templates/:id、PUT /templates/:id; 预置模板拒绝改删 - 模板按名称字母顺序排序 (Intl.Collator zh-Hans-CN, 中文按拼音/数字按数值), 前后端同口径 - 新建清单去掉场景选择器, 仅保留名称; scene 改为沿用模板自带场景 - 模板改为从右侧滑出 78vw 面板选择, 首项「不使用模板」 宏观 tab - 顺序改为 报警 → 新闻 → 油价 → 经济数据 → 策略研究 → 经济文章, 默认选中新闻 - /macro 由路由直接重定向到 /macro/news, activeFromPath 兜底同样返回 news - SwipeTabs 增加右侧溢出提示: 渐变遮罩 + 主色圆底箭头, 可点击向右翻页, 滚到末尾自动消失 - 切换 tab 时自动把激活项滚到可视区中央
529 lines
22 KiB
React
529 lines
22 KiB
React
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||
import {
|
||
Button, Dialog, DotLoading, Empty, Input, Popup, Segmented, Toast,
|
||
} from 'antd-mobile'
|
||
import { Celebrate } from '../../components/RingProgress.jsx'
|
||
import { checklistApi } from '../../api/checkin.js'
|
||
import useRefreshOnVisible from '../../hooks/useRefreshOnVisible.js'
|
||
|
||
// 清单视图: 进行中 / 已归档 / 模板 三类并列 (F-LS-*)
|
||
// - 进行中 / 已归档: 清单本体, 勾选即完成
|
||
// - 模板: 独立 CRUD (新建 / 编辑 / 删除), 按名称字母顺序排序 (中文按拼音)
|
||
// - 新建清单: 只填名称, 模板从右侧滑出面板中选择 (不再使用场景选择器)
|
||
const NAME_COLLATOR = new Intl.Collator('zh-Hans-CN', { numeric: true, sensitivity: 'base' })
|
||
const byName = (a, b) => NAME_COLLATOR.compare(String(a.name || a.title || ''), String(b.name || b.title || ''))
|
||
|
||
export default function ListView() {
|
||
const [tab, setTab] = useState('active') // active | archived | template
|
||
const [lists, setLists] = useState(null)
|
||
const [err, setErr] = useState('')
|
||
const [openId, setOpenId] = useState(null)
|
||
const [newItem, setNewItem] = useState({})
|
||
const [bursts, setBursts] = useState({})
|
||
const [templates, setTemplates] = useState(null)
|
||
const [showForm, setShowForm] = useState(false)
|
||
const [form, setForm] = useState({ title: '', templateId: '' })
|
||
const [pickerOpen, setPickerOpen] = useState(false) // 右侧模板选择面板
|
||
const [tplEdit, setTplEdit] = useState(null) // { id, name, items[] } | null
|
||
const [tplSaving, setTplSaving] = useState(false)
|
||
|
||
const archived = tab === 'archived'
|
||
|
||
const load = useCallback((silent) => {
|
||
if (!silent) setErr('')
|
||
checklistApi.lists(archived)
|
||
.then((d) => setLists(d.items || []))
|
||
.catch((e) => { if (!silent) setErr(e.message) })
|
||
}, [archived])
|
||
|
||
const loadTemplates = useCallback(() => {
|
||
checklistApi.templates()
|
||
.then((d) => setTemplates((d.items || []).slice().sort(byName)))
|
||
.catch(() => setTemplates([]))
|
||
}, [])
|
||
|
||
useEffect(() => { setLists(null); load(false) }, [load])
|
||
useRefreshOnVisible(() => { load(true); if (tab === 'template') loadTemplates() })
|
||
useEffect(() => { loadTemplates() }, [loadTemplates])
|
||
|
||
const withProgress = useCallback((list) => {
|
||
const items = list.items || []
|
||
const done = items.filter((i) => i.done).length
|
||
return {
|
||
...list,
|
||
total: items.length,
|
||
done,
|
||
percent: items.length > 0 ? done / items.length : 0,
|
||
completed: items.length > 0 && done === items.length,
|
||
}
|
||
}, [])
|
||
|
||
const patchList = (id, updater) =>
|
||
setLists((prev) => (prev || []).map((l) => (l.id === id ? updater(l) : l)))
|
||
|
||
// 勾选: 乐观更新 + 服务端确认 (F-LS-03, F-SY-06)
|
||
async function toggleItem(listId, item) {
|
||
const snapshot = lists
|
||
const next = !item.done
|
||
patchList(listId, (l) => withProgress({
|
||
...l,
|
||
items: l.items.map((i) => (i.id === item.id ? { ...i, done: next } : i)),
|
||
}))
|
||
try {
|
||
await checklistApi.updateItem(item.id, { done: next })
|
||
const l = (lists || []).find((x) => x.id === listId)
|
||
const updated = withProgress({
|
||
...l,
|
||
items: l.items.map((i) => (i.id === item.id ? { ...i, done: next } : i)),
|
||
})
|
||
if (updated.completed) setBursts((m) => ({ ...m, [listId]: (m[listId] || 0) + 1 }))
|
||
} catch (e) {
|
||
setLists(snapshot)
|
||
Toast.show({ content: e.message || '更新失败' })
|
||
}
|
||
}
|
||
|
||
async function addItem(listId, title) {
|
||
const t = String(title || '').trim()
|
||
if (!t) return
|
||
try {
|
||
const { item } = await checklistApi.addItem(listId, { title: t })
|
||
patchList(listId, (l) => withProgress({ ...l, items: [...l.items, item] }))
|
||
setNewItem((m) => ({ ...m, [listId]: '' }))
|
||
} catch (e) {
|
||
Toast.show({ content: e.message || '添加失败' })
|
||
}
|
||
}
|
||
|
||
async function removeItem(listId, itemId) {
|
||
const snapshot = lists
|
||
patchList(listId, (l) => withProgress({ ...l, items: l.items.filter((i) => i.id !== itemId) }))
|
||
try {
|
||
await checklistApi.removeItem(itemId)
|
||
} catch (e) {
|
||
setLists(snapshot)
|
||
Toast.show({ content: e.message || '删除失败' })
|
||
}
|
||
}
|
||
|
||
async function checkAll(listId, done) {
|
||
const snapshot = lists
|
||
patchList(listId, (l) => withProgress({ ...l, items: l.items.map((i) => ({ ...i, done })) }))
|
||
try {
|
||
const { item } = await checklistApi.checkAll(listId, done)
|
||
patchList(listId, () => withProgress(item))
|
||
if (item.completed) setBursts((m) => ({ ...m, [listId]: (m[listId] || 0) + 1 }))
|
||
} catch (e) {
|
||
setLists(snapshot)
|
||
Toast.show({ content: e.message || '操作失败' })
|
||
}
|
||
}
|
||
|
||
// ---------- 清单 ----------
|
||
|
||
function openCreateForm(templateId = '') {
|
||
setForm({ title: '', templateId })
|
||
setShowForm(true)
|
||
}
|
||
|
||
async function createList() {
|
||
const title = String(form.title || '').trim()
|
||
if (!title) return Toast.show({ content: '请填写清单名称' })
|
||
const tpl = templateById(form.templateId)
|
||
try {
|
||
await checklistApi.create({
|
||
title,
|
||
// 场景不再由用户选择: 沿用模板自带的场景 (无模板则为空), 兼容历史数据展示
|
||
scene: tpl?.scene || '',
|
||
templateId: form.templateId || undefined,
|
||
})
|
||
setShowForm(false)
|
||
setForm({ title: '', templateId: '' })
|
||
setTab('active')
|
||
load(true)
|
||
} catch (e) {
|
||
Toast.show({ content: e.message || '创建失败' })
|
||
}
|
||
}
|
||
|
||
async function duplicate(list) {
|
||
try {
|
||
await checklistApi.duplicate(list.id)
|
||
Toast.show({ content: '已复制为新清单' })
|
||
load(true)
|
||
} catch (e) {
|
||
Toast.show({ content: e.message || '复制失败' })
|
||
}
|
||
}
|
||
|
||
async function saveAsTemplate(list) {
|
||
const items = (list.items || []).map((i) => ({ title: i.title, note: i.note }))
|
||
if (!items.length) return Toast.show({ content: '空清单无法存为模板' })
|
||
try {
|
||
await checklistApi.createTemplate({ name: list.title, scene: list.scene || '', items })
|
||
Toast.show({ content: '已保存为模板' })
|
||
loadTemplates()
|
||
} catch (e) {
|
||
Toast.show({ content: e.message || '保存失败' })
|
||
}
|
||
}
|
||
|
||
async function toggleArchive(list) {
|
||
try {
|
||
await checklistApi.update(list.id, { archived: !list.archived })
|
||
Toast.show({ content: list.archived ? '已恢复到进行中' : '已归档' })
|
||
load(true)
|
||
} catch (e) {
|
||
Toast.show({ content: e.message || '操作失败' })
|
||
}
|
||
}
|
||
|
||
async function removeList(list) {
|
||
const ok = await Dialog.confirm({ content: `删除「${list.title}」?`, confirmText: '删除', cancelText: '取消' })
|
||
if (!ok) return
|
||
try {
|
||
await checklistApi.remove(list.id)
|
||
load(true)
|
||
} catch (e) {
|
||
Toast.show({ content: e.message || '删除失败' })
|
||
}
|
||
}
|
||
|
||
// ---------- 模板 CRUD ----------
|
||
|
||
function templateById(id) {
|
||
if (!id) return null
|
||
return (templates || []).find((t) => String(t.id) === String(id)) || null
|
||
}
|
||
|
||
function newTemplate() {
|
||
setTplEdit({ id: null, name: '', items: [{ title: '' }], builtin: false })
|
||
}
|
||
|
||
function editTemplate(tpl) {
|
||
if (tpl.builtin) return Toast.show({ content: '系统预置模板不可编辑' })
|
||
setTplEdit({
|
||
id: tpl.id,
|
||
name: tpl.name,
|
||
builtin: false,
|
||
items: (Array.isArray(tpl.items) ? tpl.items : []).map((i) => ({
|
||
title: String(i?.title || ''),
|
||
note: String(i?.note || ''),
|
||
})),
|
||
})
|
||
}
|
||
|
||
function setTplItem(idx, title) {
|
||
setTplEdit((t) => ({ ...t, items: t.items.map((it, i) => (i === idx ? { ...it, title } : it)) }))
|
||
}
|
||
|
||
function addTplItem() {
|
||
setTplEdit((t) => ({ ...t, items: [...t.items, { title: '' }] }))
|
||
}
|
||
|
||
function removeTplItem(idx) {
|
||
setTplEdit((t) => ({ ...t, items: t.items.filter((_, i) => i !== idx) }))
|
||
}
|
||
|
||
async function saveTemplate() {
|
||
if (!tplEdit) return
|
||
const name = String(tplEdit.name || '').trim()
|
||
const items = tplEdit.items
|
||
.map((i) => ({ title: String(i.title || '').trim(), note: String(i.note || '') }))
|
||
.filter((i) => i.title)
|
||
if (!name) return Toast.show({ content: '请填写模板名称' })
|
||
if (name.length > 20) return Toast.show({ content: '模板名称不能超过 20 个字' })
|
||
if (!items.length) return Toast.show({ content: '模板至少需要一个检查项' })
|
||
setTplSaving(true)
|
||
try {
|
||
if (tplEdit.id) await checklistApi.updateTemplate(tplEdit.id, { name, items })
|
||
else await checklistApi.createTemplate({ name, items })
|
||
Toast.show({ content: tplEdit.id ? '已保存' : '模板已创建' })
|
||
setTplEdit(null)
|
||
loadTemplates()
|
||
} catch (e) {
|
||
Toast.show({ content: e.message || '保存失败' })
|
||
} finally {
|
||
setTplSaving(false)
|
||
}
|
||
}
|
||
|
||
async function removeTemplate(tpl) {
|
||
if (tpl.builtin) return Toast.show({ content: '系统预置模板不可删除' })
|
||
const ok = await Dialog.confirm({
|
||
content: `删除模板「${tpl.name}」?已创建的清单不受影响。`,
|
||
confirmText: '删除',
|
||
cancelText: '取消',
|
||
})
|
||
if (!ok) return
|
||
try {
|
||
await checklistApi.removeTemplate(tpl.id)
|
||
Toast.show({ content: '模板已删除' })
|
||
loadTemplates()
|
||
} catch (e) {
|
||
Toast.show({ content: e.message || '删除失败' })
|
||
}
|
||
}
|
||
|
||
const sorted = useMemo(() => lists || [], [lists])
|
||
const templateCount = (t) => (Array.isArray(t.items) ? t.items.length : 0)
|
||
const selectedTpl = templateById(form.templateId)
|
||
|
||
return (
|
||
<div className="list-view">
|
||
<div className="list-view-head">
|
||
<Segmented
|
||
options={[
|
||
{ label: '进行中', value: 'active' },
|
||
{ label: '已归档', value: 'archived' },
|
||
{ label: '模板', value: 'template' },
|
||
]}
|
||
value={tab}
|
||
onChange={(v) => setTab(v)}
|
||
/>
|
||
{tab === 'template'
|
||
? <Button size="mini" color="primary" onClick={newTemplate}>+ 新建模板</Button>
|
||
: <Button size="mini" color="primary" onClick={() => openCreateForm()}>+ 新建</Button>}
|
||
</div>
|
||
|
||
{/* ---------- 模板分类 ---------- */}
|
||
{tab === 'template' && (
|
||
<>
|
||
{templates === null && <div className="monitor-loading"><DotLoading /> 加载中...</div>}
|
||
{templates !== null && templates.length === 0 && (
|
||
<Empty description="还没有模板">
|
||
<Button color="primary" size="small" onClick={newTemplate}>创建第一个模板</Button>
|
||
</Empty>
|
||
)}
|
||
<div className="template-wrap">
|
||
{(templates || []).map((tpl) => (
|
||
<div key={tpl.id} className="template-card">
|
||
<div className="template-head">
|
||
<span className="template-name">{tpl.name}</span>
|
||
{tpl.builtin && <span className="template-badge">预置</span>}
|
||
<span className="template-count">{templateCount(tpl)} 项</span>
|
||
</div>
|
||
<div className="template-items">
|
||
{(Array.isArray(tpl.items) ? tpl.items : []).slice(0, 4).map((it, i) => (
|
||
<span key={i} className="template-item-chip">{it.title}</span>
|
||
))}
|
||
{templateCount(tpl) > 4 && (
|
||
<span className="template-item-chip more">+{templateCount(tpl) - 4}</span>
|
||
)}
|
||
</div>
|
||
<div className="template-actions">
|
||
<span className="habit-link" onClick={() => openCreateForm(String(tpl.id))}>由此新建</span>
|
||
{!tpl.builtin && <span className="habit-link" onClick={() => editTemplate(tpl)}>编辑</span>}
|
||
{!tpl.builtin && <span className="habit-link danger" onClick={() => removeTemplate(tpl)}>删除</span>}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
{/* ---------- 清单列表 ---------- */}
|
||
{tab !== 'template' && (
|
||
<>
|
||
{err && <div className="error-banner">{err}<span className="retry-link" onClick={() => load(false)}>重试</span></div>}
|
||
{lists === null && !err && <div className="monitor-loading"><DotLoading /> 加载中...</div>}
|
||
{lists !== null && !err && sorted.length === 0 && (
|
||
<Empty description={archived ? '暂无归档清单' : '还没有检查清单'}>
|
||
{!archived && <Button color="primary" size="small" onClick={() => openCreateForm()}>新建清单</Button>}
|
||
</Empty>
|
||
)}
|
||
|
||
<div className="checklist-wrap">
|
||
{sorted.map((list) => {
|
||
const open = openId === list.id
|
||
return (
|
||
<div key={list.id} className={'checklist-card' + (list.completed ? ' done' : '')}>
|
||
<div className="checklist-head" onClick={() => setOpenId(open ? null : list.id)}>
|
||
<div className="checklist-title-row">
|
||
<span className="checklist-title">{list.title}</span>
|
||
{list.scene && <span className="checklist-scene">{list.scene}</span>}
|
||
</div>
|
||
<div className="checklist-progress-row">
|
||
<div className="checklist-bar">
|
||
<div
|
||
className="checklist-bar-fill"
|
||
style={{ width: Math.round((list.percent || 0) * 100) + '%' }}
|
||
/>
|
||
</div>
|
||
<span className="checklist-count">{list.done}/{list.total}</span>
|
||
<span className={'checklist-arrow' + (open ? ' open' : '')}>›</span>
|
||
</div>
|
||
</div>
|
||
|
||
{open && (
|
||
<div className="checklist-body">
|
||
{(list.items || []).map((item) => (
|
||
<div key={item.id} className={'checklist-item' + (item.done ? ' done' : '')}>
|
||
<span
|
||
className="check-box"
|
||
onClick={(e) => { e.stopPropagation(); toggleItem(list.id, item) }}
|
||
>
|
||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none">
|
||
<path d="M4 12.5l5 5L20 6.5" stroke="#fff" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" />
|
||
</svg>
|
||
</span>
|
||
<span className="checklist-item-title" onClick={() => toggleItem(list.id, item)}>
|
||
{item.title}
|
||
{item.note && <em>{item.note}</em>}
|
||
</span>
|
||
<span className="checklist-item-del" onClick={() => removeItem(list.id, item.id)}>×</span>
|
||
</div>
|
||
))}
|
||
|
||
<div className="checklist-add">
|
||
<Input
|
||
placeholder="添加检查项"
|
||
value={newItem[list.id] || ''}
|
||
onChange={(v) => setNewItem((m) => ({ ...m, [list.id]: v }))}
|
||
onEnterPress={() => addItem(list.id, newItem[list.id])}
|
||
/>
|
||
<Button size="mini" color="primary" onClick={() => addItem(list.id, newItem[list.id])}>添加</Button>
|
||
</div>
|
||
|
||
<div className="checklist-actions">
|
||
<span className="habit-link" onClick={() => checkAll(list.id, true)}>全选</span>
|
||
<span className="habit-link" onClick={() => checkAll(list.id, false)}>全不选</span>
|
||
<span className="habit-link" onClick={() => duplicate(list)}>复制</span>
|
||
<span className="habit-link" onClick={() => saveAsTemplate(list)}>存为模板</span>
|
||
<span className="habit-link" onClick={() => toggleArchive(list)}>{list.archived ? '恢复' : '归档'}</span>
|
||
<span className="habit-link danger" onClick={() => removeList(list)}>删除</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
<Celebrate fire={bursts[list.id] || 0} color="#00b578" />
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
{/* ---------- 新建清单 (名称 + 模板, 无场景) ---------- */}
|
||
<Popup
|
||
visible={showForm}
|
||
onMaskClick={() => setShowForm(false)}
|
||
bodyStyle={{ borderTopLeftRadius: 12, borderTopRightRadius: 12, paddingBottom: 16 }}
|
||
>
|
||
<div className="form-popup">
|
||
<div className="form-title">新建检查清单</div>
|
||
<div className="form-field">
|
||
<label>名称</label>
|
||
<Input
|
||
placeholder="例如:出差 · 杭州"
|
||
value={form.title}
|
||
maxLength={20}
|
||
onChange={(v) => setForm((f) => ({ ...f, title: v }))}
|
||
/>
|
||
</div>
|
||
<div className="form-field">
|
||
<label>模板</label>
|
||
<div className="template-pick-row" onClick={() => setPickerOpen(true)}>
|
||
<span className={selectedTpl ? 'template-pick-name' : 'template-pick-name placeholder'}>
|
||
{selectedTpl ? selectedTpl.name : '不使用模板(空白清单)'}
|
||
</span>
|
||
<span className="template-pick-meta">
|
||
{selectedTpl ? `${templateCount(selectedTpl)} 项` : ''}
|
||
<span className="template-pick-arrow">›</span>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div className="form-actions">
|
||
<Button block color="primary" onClick={createList}>创建</Button>
|
||
<Button block fill="outline" onClick={() => setShowForm(false)} style={{ marginTop: 8 }}>取消</Button>
|
||
</div>
|
||
</div>
|
||
</Popup>
|
||
|
||
{/* ---------- 模板选择: 右侧滑出面板 ---------- */}
|
||
<Popup
|
||
visible={pickerOpen}
|
||
position="right"
|
||
onMaskClick={() => setPickerOpen(false)}
|
||
bodyStyle={{ width: '78vw', maxWidth: 340, height: '100%', display: 'flex', flexDirection: 'column' }}
|
||
>
|
||
<div className="tpl-picker">
|
||
<div className="tpl-picker-head">
|
||
<span className="tpl-picker-title">选择模板</span>
|
||
<span className="tpl-picker-close" onClick={() => setPickerOpen(false)}>×</span>
|
||
</div>
|
||
<div className="tpl-picker-tip">按名称字母顺序排列</div>
|
||
<div className="tpl-picker-list">
|
||
<div
|
||
className={'tpl-picker-item' + (!form.templateId ? ' active' : '')}
|
||
onClick={() => { setForm((f) => ({ ...f, templateId: '' })); setPickerOpen(false) }}
|
||
>
|
||
<span className="tpl-picker-name">不使用模板</span>
|
||
<span className="tpl-picker-sub">空白清单</span>
|
||
</div>
|
||
{(templates || []).map((tpl) => (
|
||
<div
|
||
key={tpl.id}
|
||
className={'tpl-picker-item' + (String(tpl.id) === String(form.templateId) ? ' active' : '')}
|
||
onClick={() => { setForm((f) => ({ ...f, templateId: String(tpl.id) })); setPickerOpen(false) }}
|
||
>
|
||
<span className="tpl-picker-name">
|
||
{tpl.name}
|
||
{tpl.builtin && <span className="template-badge">预置</span>}
|
||
</span>
|
||
<span className="tpl-picker-sub">{templateCount(tpl)} 项</span>
|
||
</div>
|
||
))}
|
||
{templates !== null && templates.length === 0 && (
|
||
<div className="tpl-picker-empty">还没有可用模板</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</Popup>
|
||
|
||
{/* ---------- 模板编辑 ---------- */}
|
||
<Popup
|
||
visible={!!tplEdit}
|
||
onMaskClick={() => setTplEdit(null)}
|
||
bodyStyle={{ borderTopLeftRadius: 12, borderTopRightRadius: 12, paddingBottom: 16, maxHeight: '82vh', overflowY: 'auto' }}
|
||
>
|
||
{tplEdit && (
|
||
<div className="form-popup">
|
||
<div className="form-title">{tplEdit.id ? '编辑模板' : '新建模板'}</div>
|
||
<div className="form-field">
|
||
<label>模板名称</label>
|
||
<Input
|
||
placeholder="例如:出差 / 露营"
|
||
value={tplEdit.name}
|
||
maxLength={20}
|
||
onChange={(v) => setTplEdit((t) => ({ ...t, name: v }))}
|
||
/>
|
||
</div>
|
||
<div className="form-field">
|
||
<label>检查项({tplEdit.items.length})</label>
|
||
<div className="tpl-edit-items">
|
||
{tplEdit.items.map((it, idx) => (
|
||
<div key={idx} className="tpl-edit-item">
|
||
<span className="tpl-edit-index">{idx + 1}</span>
|
||
<Input
|
||
placeholder="检查项内容"
|
||
value={it.title}
|
||
maxLength={40}
|
||
onChange={(v) => setTplItem(idx, v)}
|
||
/>
|
||
<span className="checklist-item-del" onClick={() => removeTplItem(idx)}>×</span>
|
||
</div>
|
||
))}
|
||
<Button size="mini" fill="outline" onClick={addTplItem} style={{ marginTop: 8 }}>+ 添加检查项</Button>
|
||
</div>
|
||
</div>
|
||
<div className="form-actions">
|
||
<Button block color="primary" loading={tplSaving} onClick={saveTemplate}>保存</Button>
|
||
<Button block fill="outline" onClick={() => setTplEdit(null)} style={{ marginTop: 8 }}>取消</Button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</Popup>
|
||
</div>
|
||
)
|
||
}
|