feat(auth): access token 缩到 2 小时 + refresh token 机制 #3
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
-2
@@ -5,8 +5,8 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/admin/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>admin</title>
|
||||
<script type="module" crossorigin src="/admin/assets/index-DMg3rix-.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/admin/assets/index-D4fxTaca.css">
|
||||
<script type="module" crossorigin src="/admin/assets/index-DkaMHmZz.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/admin/assets/index-0lrkDIfq.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -10,6 +10,8 @@ import Posts from '@/pages/Posts'
|
||||
import Comments from '@/pages/Comments'
|
||||
import Articles from '@/pages/Articles'
|
||||
import CareTemplates from '@/pages/CareTemplates'
|
||||
import RecordTypes from '@/pages/RecordTypes'
|
||||
import Breeds from '@/pages/Breeds'
|
||||
import CommunityBot from '@/pages/CommunityBot'
|
||||
import Feedback from '@/pages/Feedback'
|
||||
import Members from '@/pages/Members'
|
||||
@@ -37,6 +39,8 @@ export default function App() {
|
||||
<Route path="comments" element={<Comments />} />
|
||||
<Route path="articles" element={<Articles />} />
|
||||
<Route path="care-templates" element={<CareTemplates />} />
|
||||
<Route path="record-types" element={<RecordTypes />} />
|
||||
<Route path="breeds" element={<Breeds />} />
|
||||
<Route path="community-bot" element={<CommunityBot />} />
|
||||
<Route path="feedback" element={<Feedback />} />
|
||||
<Route path="members" element={<Members />} />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NavLink, Outlet, useNavigate } from 'react-router-dom'
|
||||
import { LayoutDashboard, Users, PawPrint, MessageSquare, MessagesSquare, FileText, ClipboardList, Sparkles, MessageCircle, Crown, LogOut } from 'lucide-react'
|
||||
import { LayoutDashboard, Users, PawPrint, MessageSquare, MessagesSquare, FileText, ClipboardList, ListChecks, Dna, Sparkles, MessageCircle, Crown, LogOut } from 'lucide-react'
|
||||
import { api, clearToken } from '@/lib/api'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
@@ -12,6 +12,8 @@ const nav = [
|
||||
{ to: '/comments', label: '评论管理', icon: MessagesSquare },
|
||||
{ to: '/articles', label: '文章管理', icon: FileText },
|
||||
{ to: '/care-templates', label: '养护模板', icon: ClipboardList },
|
||||
{ to: '/record-types', label: '记录事项', icon: ListChecks },
|
||||
{ to: '/breeds', label: '品种管理', icon: Dna },
|
||||
{ to: '/community-bot', label: '社区运营', icon: Sparkles },
|
||||
{ to: '/feedback', label: '意见反馈', icon: MessageCircle },
|
||||
{ to: '/members', label: '会员管理', icon: Crown },
|
||||
|
||||
@@ -143,6 +143,17 @@ export const api = {
|
||||
),
|
||||
saveAIQuota: (c: any) => http.put<any, any>('/admin/ai-quota', c),
|
||||
|
||||
// 记录类型(小程序「记一笔」的那些事项)
|
||||
recordTypes: () => http.get<any, any[]>('/admin/record-types'),
|
||||
saveRecordType: (t: any) => http.post('/admin/record-types', t),
|
||||
deleteRecordType: (id: string) => http.delete(`/admin/record-types/${id}`),
|
||||
|
||||
// 品种
|
||||
breeds: (species?: string) =>
|
||||
http.get<any, any[]>('/admin/breeds', { params: species ? { species } : {} }),
|
||||
saveBreed: (b: any) => http.post('/admin/breeds', b),
|
||||
deleteBreed: (id: string) => http.delete(`/admin/breeds/${id}`),
|
||||
|
||||
communityBot: () =>
|
||||
http.get<any, { enabled: boolean; daily_count: number; start_hour: number; end_hour: number }>(
|
||||
'/admin/community-bot',
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { api } from '@/lib/api'
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
|
||||
type Breed = {
|
||||
id: string
|
||||
species: string
|
||||
name: string
|
||||
initial: string
|
||||
sort: number
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
const empty: Breed = { id: '', species: 'cat', name: '', initial: '', sort: 0, enabled: true }
|
||||
const SPECIES = [
|
||||
{ key: 'cat', label: '猫' },
|
||||
{ key: 'dog', label: '狗' },
|
||||
]
|
||||
|
||||
export default function Breeds() {
|
||||
const [species, setSpecies] = useState('cat')
|
||||
const [list, setList] = useState<Breed[]>([])
|
||||
const [open, setOpen] = useState(false)
|
||||
const [form, setForm] = useState<Breed>(empty)
|
||||
const [err, setErr] = useState('')
|
||||
|
||||
function reload() {
|
||||
api
|
||||
.breeds(species)
|
||||
.then((r) => setList(r || []))
|
||||
.catch(() => setList([]))
|
||||
}
|
||||
useEffect(reload, [species])
|
||||
|
||||
function create() {
|
||||
setForm({ ...empty, species })
|
||||
setErr('')
|
||||
setOpen(true)
|
||||
}
|
||||
function edit(b: Breed) {
|
||||
setForm({ ...b })
|
||||
setErr('')
|
||||
setOpen(true)
|
||||
}
|
||||
async function save() {
|
||||
setErr('')
|
||||
try {
|
||||
await api.saveBreed(form)
|
||||
setOpen(false)
|
||||
reload()
|
||||
} catch (e: any) {
|
||||
// 后端会明确说是重名还是首字母不合法,直接透出来比一句「保存失败」有用
|
||||
setErr(e?.message || '保存失败')
|
||||
}
|
||||
}
|
||||
async function del(b: Breed) {
|
||||
if (!confirm(`删除「${b.name}」?`)) return
|
||||
try {
|
||||
await api.deleteBreed(b.id)
|
||||
reload()
|
||||
} catch (e: any) {
|
||||
alert(e?.message || '删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
const set = (k: keyof Breed) => (e: any) => setForm({ ...form, [k]: e.target.value })
|
||||
|
||||
// 按首字母分段展示,和小程序端的 A-Z 索引对得上
|
||||
const groups: Record<string, Breed[]> = {}
|
||||
list.forEach((b) => {
|
||||
const k = (b.initial || '#').toUpperCase()
|
||||
;(groups[k] = groups[k] || []).push(b)
|
||||
})
|
||||
const letters = Object.keys(groups).sort()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">品种管理</h1>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
建档第二步的品种选择器读这里。首字母要手填 —— 多音字(藏獒、柴犬)自动转拼音常出错。
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={create}>新增品种</Button>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 flex gap-2">
|
||||
{SPECIES.map((s) => (
|
||||
<Button
|
||||
key={s.key}
|
||||
variant={species === s.key ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setSpecies(s.key)}
|
||||
>
|
||||
{s.label}
|
||||
</Button>
|
||||
))}
|
||||
<span className="ml-2 self-center text-sm text-muted-foreground">
|
||||
共 {list.length} 种,{letters.length} 个首字母
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-16">首字母</TableHead>
|
||||
<TableHead>品种</TableHead>
|
||||
<TableHead className="w-20">排序</TableHead>
|
||||
<TableHead className="w-20">状态</TableHead>
|
||||
<TableHead className="w-32 text-right">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{letters.map((L) =>
|
||||
groups[L].map((b, i) => (
|
||||
<TableRow key={b.id}>
|
||||
<TableCell className="font-mono text-muted-foreground">
|
||||
{i === 0 ? L : ''}
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">{b.name}</TableCell>
|
||||
<TableCell className="text-muted-foreground">{b.sort}</TableCell>
|
||||
<TableCell>
|
||||
{b.enabled ? (
|
||||
<Badge>启用</Badge>
|
||||
) : (
|
||||
<Badge variant="secondary">停用</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button variant="ghost" size="sm" onClick={() => edit(b)}>
|
||||
编辑
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => del(b)}>
|
||||
删除
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)),
|
||||
)}
|
||||
{!list.length && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="py-10 text-center text-muted-foreground">
|
||||
还没有品种
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{form.id ? '编辑品种' : '新增品种'}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label>物种</Label>
|
||||
<div className="flex gap-2">
|
||||
{SPECIES.map((s) => (
|
||||
<Button
|
||||
key={s.key}
|
||||
type="button"
|
||||
variant={form.species === s.key ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setForm({ ...form, species: s.key })}
|
||||
>
|
||||
{s.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>品种名</Label>
|
||||
<Input value={form.name} onChange={set('name')} placeholder="例如:布偶猫" />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>首字母(A-Z,一位)</Label>
|
||||
<Input
|
||||
value={form.initial}
|
||||
maxLength={1}
|
||||
onChange={(e) => setForm({ ...form, initial: e.target.value.toUpperCase() })}
|
||||
placeholder="B"
|
||||
className="w-20 font-mono"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
按国内常用读法填。多音字自己定:藏獒填 Z,柴犬填 C。
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>排序(同首字母内,小的在前)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={form.sort}
|
||||
onChange={(e) => setForm({ ...form, sort: Number(e.target.value) || 0 })}
|
||||
className="w-28"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
id="bd-enabled"
|
||||
type="checkbox"
|
||||
checked={form.enabled}
|
||||
onChange={(e) => setForm({ ...form, enabled: e.target.checked })}
|
||||
/>
|
||||
<Label htmlFor="bd-enabled" className="cursor-pointer">
|
||||
启用(停用后建档时选不到,已填这个品种的档案不受影响)
|
||||
</Label>
|
||||
</div>
|
||||
{err && <p className="text-sm text-destructive">{err}</p>}
|
||||
<Button onClick={save}>保存</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { api } from '@/lib/api'
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
|
||||
type Field = { kind: string; label: string; unit?: string; options?: string[]; to_category: boolean }
|
||||
type RT = {
|
||||
id: string
|
||||
code: string
|
||||
label: string
|
||||
icon: string
|
||||
group: string
|
||||
sort: number
|
||||
enabled: boolean
|
||||
form: string
|
||||
locked: boolean
|
||||
fields_raw: string
|
||||
fields: Field[]
|
||||
}
|
||||
|
||||
const empty: any = {
|
||||
id: '', code: '', label: '', icon: 'note', group: 'daily',
|
||||
sort: 0, enabled: true, form: 'simple', locked: false, fields_raw: '',
|
||||
}
|
||||
const GROUPS = [
|
||||
{ key: 'daily', label: '日常' },
|
||||
{ key: 'health', label: '健康' },
|
||||
{ key: 'care', label: '洗护' },
|
||||
{ key: 'clean', label: '清洁' },
|
||||
]
|
||||
const gLabel = (k: string) => GROUPS.find((g) => g.key === k)?.label || k
|
||||
|
||||
// 字段配置那段紧凑写法的说明。放在弹窗里而不是文档里——
|
||||
// 这东西一年用两次,去翻文档的成本比它本身还高
|
||||
const HINT = `number:体重:kg 数值,落 num_value
|
||||
options:状态:正常|软便|拉稀 单选,落 category
|
||||
options:症状:呕吐|拉稀:- 末尾 - 表示不落 category(只进标题)
|
||||
text:吃了什么 文本,只进标题
|
||||
(留空) 只要「时间 + 描述 + 照片」`
|
||||
|
||||
export default function RecordTypes() {
|
||||
const [list, setList] = useState<RT[]>([])
|
||||
const [open, setOpen] = useState(false)
|
||||
const [form, setForm] = useState<any>(empty)
|
||||
const [err, setErr] = useState('')
|
||||
|
||||
function reload() {
|
||||
api
|
||||
.recordTypes()
|
||||
.then((r) => setList(r || []))
|
||||
.catch(() => setList([]))
|
||||
}
|
||||
useEffect(reload, [])
|
||||
|
||||
function create() {
|
||||
setForm({ ...empty })
|
||||
setErr('')
|
||||
setOpen(true)
|
||||
}
|
||||
function edit(t: RT) {
|
||||
setForm({ ...t })
|
||||
setErr('')
|
||||
setOpen(true)
|
||||
}
|
||||
async function save() {
|
||||
setErr('')
|
||||
try {
|
||||
await api.saveRecordType(form)
|
||||
setOpen(false)
|
||||
reload()
|
||||
} catch (e: any) {
|
||||
// 字段配置写错了后端会说第几行错在哪,原样透出
|
||||
setErr(e?.message || '保存失败')
|
||||
}
|
||||
}
|
||||
async function del(t: RT) {
|
||||
if (!confirm(`删除「${t.label}」?`)) return
|
||||
try {
|
||||
await api.deleteRecordType(t.id)
|
||||
reload()
|
||||
} catch (e: any) {
|
||||
alert(e?.message || '删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
const set = (k: string) => (e: any) => setForm({ ...form, [k]: e.target.value })
|
||||
|
||||
// 字段配置渲染成人话,省得盯着 DSL 猜
|
||||
function fieldsText(t: RT) {
|
||||
if (!t.fields?.length) return <span className="text-muted-foreground">时间 + 描述 + 照片</span>
|
||||
return (
|
||||
<span className="space-x-2">
|
||||
{t.fields.map((f, i) => (
|
||||
<span key={i} className="whitespace-nowrap text-xs">
|
||||
{f.label}
|
||||
{f.kind === 'number' && `·数值${f.unit || ''}`}
|
||||
{f.kind === 'text' && '·文本'}
|
||||
{f.kind === 'options' && `·单选${f.to_category ? '★' : ''}`}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const byGroup: Record<string, RT[]> = {}
|
||||
list.forEach((t) => ((byGroup[t.group] = byGroup[t.group] || []).push(t)))
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">记录事项</h1>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
小程序「记一笔」的事项和它们的表单字段。★ 标记的单选会落到 category ——
|
||||
周报的高风险数、账单的分类统计都读这一列。
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={create}>新增事项</Button>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-20">分组</TableHead>
|
||||
<TableHead className="w-28">名称</TableHead>
|
||||
<TableHead className="w-28">code</TableHead>
|
||||
<TableHead>表单字段</TableHead>
|
||||
<TableHead className="w-16">排序</TableHead>
|
||||
<TableHead className="w-24">状态</TableHead>
|
||||
<TableHead className="w-32 text-right">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{GROUPS.map((g) =>
|
||||
(byGroup[g.key] || []).map((t, i) => (
|
||||
<TableRow key={t.id}>
|
||||
<TableCell className="text-muted-foreground">{i === 0 ? g.label : ''}</TableCell>
|
||||
<TableCell className="font-medium">{t.label}</TableCell>
|
||||
<TableCell className="font-mono text-xs text-muted-foreground">{t.code}</TableCell>
|
||||
<TableCell>{fieldsText(t)}</TableCell>
|
||||
<TableCell className="text-muted-foreground">{t.sort}</TableCell>
|
||||
<TableCell>
|
||||
{t.enabled ? <Badge>启用</Badge> : <Badge variant="secondary">停用</Badge>}
|
||||
{t.locked && (
|
||||
<Badge variant="outline" className="ml-1">
|
||||
内置
|
||||
</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button variant="ghost" size="sm" onClick={() => edit(t)}>
|
||||
编辑
|
||||
</Button>
|
||||
{!t.locked && (
|
||||
<Button variant="ghost" size="sm" onClick={() => del(t)}>
|
||||
删除
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)),
|
||||
)}
|
||||
{!list.length && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={7} className="py-10 text-center text-muted-foreground">
|
||||
还没有事项 —— 小程序的记录页会是空的
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{form.id ? `编辑「${form.label}」` : '新增事项'}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label>名称</Label>
|
||||
<Input value={form.label} onChange={set('label')} placeholder="洗澡" />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>code(落库的 type,建过记录后不能改)</Label>
|
||||
<Input
|
||||
value={form.code}
|
||||
onChange={set('code')}
|
||||
placeholder="bath"
|
||||
className="font-mono"
|
||||
disabled={form.locked}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label>分组</Label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{GROUPS.map((g) => (
|
||||
<Button
|
||||
key={g.key}
|
||||
type="button"
|
||||
variant={form.group === g.key ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setForm({ ...form, group: g.key })}
|
||||
>
|
||||
{g.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>图标名(pt-icon)</Label>
|
||||
<Input value={form.icon} onChange={set('icon')} placeholder="bath" className="font-mono" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>表单字段(一行一个,留空则只有「时间 + 描述 + 照片」)</Label>
|
||||
<textarea
|
||||
className="min-h-[120px] w-full rounded-md border border-input bg-card px-3 py-2 font-mono text-sm shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
value={form.fields_raw || ''}
|
||||
onChange={set('fields_raw')}
|
||||
placeholder="number:体重:kg"
|
||||
/>
|
||||
<pre className="whitespace-pre-wrap rounded bg-muted p-3 text-xs text-muted-foreground">
|
||||
{HINT}
|
||||
</pre>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
一个事项里最多一个字段能落 category。多出来的那个末尾加 <code>:-</code>。
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label>排序</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={form.sort}
|
||||
onChange={(e) => setForm({ ...form, sort: Number(e.target.value) || 0 })}
|
||||
className="w-24"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
id="rt-enabled"
|
||||
type="checkbox"
|
||||
checked={form.enabled}
|
||||
onChange={(e) => setForm({ ...form, enabled: e.target.checked })}
|
||||
/>
|
||||
<Label htmlFor="rt-enabled" className="cursor-pointer">
|
||||
启用
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
{form.locked && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
这是内置事项:统计和分析逻辑依赖它的 code,不能删也不能改 code。想隐藏请改成停用。
|
||||
</p>
|
||||
)}
|
||||
{err && <p className="text-sm text-destructive">{err}</p>}
|
||||
<Button onClick={save}>保存</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user