添加文章管理和文章分组页面
This commit is contained in:
@@ -112,7 +112,7 @@ export default function DashboardPage() {
|
||||
// 处理取消
|
||||
const handleCancel = () => {
|
||||
setShowCustomPicker(false)
|
||||
setTimeRange("7d")
|
||||
setTimeRange("7d")
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -232,18 +232,18 @@ export default function DashboardPage() {
|
||||
{/* 点击自定义时显示 */}
|
||||
{showCustomPicker && (
|
||||
<div className="flex gap-2 items-center p-1 rounded-md">
|
||||
<input
|
||||
type="date"
|
||||
<input
|
||||
type="date"
|
||||
className="px-2 py-1 border rounded text-sm bg-background"
|
||||
value={startDate}
|
||||
onChange={(e) => setStartDate(e.target.value)}
|
||||
onChange={e => setStartDate(e.target.value)}
|
||||
/>
|
||||
<span className="text-muted-foreground">~</span>
|
||||
<input
|
||||
type="date"
|
||||
<input
|
||||
type="date"
|
||||
className="px-2 py-1 border rounded text-sm bg-background"
|
||||
value={endDate}
|
||||
onChange={(e) => setEndDate(e.target.value)}
|
||||
onChange={e => setEndDate(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
onClick={handleDateConfirm}
|
||||
@@ -436,4 +436,4 @@ function ResourceItem({
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,11 @@ import {
|
||||
DollarSign,
|
||||
DoorClosedIcon,
|
||||
FolderCode,
|
||||
FolderTree,
|
||||
Home,
|
||||
KeyRound,
|
||||
type LucideIcon,
|
||||
Newspaper,
|
||||
Package,
|
||||
ScanSearch,
|
||||
Shield,
|
||||
@@ -217,6 +219,16 @@ const menuSections: { title: string; items: NavItemProps[] }[] = [
|
||||
label: "产品管理",
|
||||
requiredScope: ScopeProductRead,
|
||||
},
|
||||
{
|
||||
href: "/articles",
|
||||
icon: Newspaper,
|
||||
label: "文章管理",
|
||||
},
|
||||
{
|
||||
href: "/article-groups",
|
||||
icon: FolderTree,
|
||||
label: "文章分组",
|
||||
},
|
||||
{
|
||||
href: "/discount",
|
||||
icon: SquarePercent,
|
||||
|
||||
@@ -59,6 +59,7 @@ export default function Appbar(props: { admin: Admin }) {
|
||||
dashboard: "控制台",
|
||||
content: "内容管理",
|
||||
articles: "文章管理",
|
||||
"article-groups": "文章分组",
|
||||
media: "媒体库",
|
||||
user: "客户认领",
|
||||
roles: "角色权限",
|
||||
@@ -82,9 +83,12 @@ export default function Appbar(props: { admin: Admin }) {
|
||||
balance: "余额明细",
|
||||
gateway: "网关列表",
|
||||
couponList: "已发放优惠券",
|
||||
new: "新建文章",
|
||||
}
|
||||
|
||||
return labels[path] || path
|
||||
if (labels[path]) return labels[path]
|
||||
if (/^\d+$/.test(path)) return "编辑文章"
|
||||
return path
|
||||
}
|
||||
|
||||
const breadcrumbs = generateBreadcrumbs()
|
||||
|
||||
208
src/app/(root)/article-groups/page.tsx
Normal file
208
src/app/(root)/article-groups/page.tsx
Normal file
@@ -0,0 +1,208 @@
|
||||
"use client"
|
||||
|
||||
import { Loader2 } from "lucide-react"
|
||||
import { Suspense, useCallback, useState } from "react"
|
||||
import { toast } from "sonner"
|
||||
import {
|
||||
createArticleGroup,
|
||||
getArticleGroupPage,
|
||||
removeArticleGroup,
|
||||
updateArticleGroup,
|
||||
} from "@/actions/article-group"
|
||||
import { DataTable, useDataTable } from "@/components/data-table"
|
||||
import { Page } from "@/components/page"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import type { ArticleGroup } from "@/models/article-group"
|
||||
import { formatDate } from "@/models/formatDate"
|
||||
|
||||
export default function ArticleGroupPage() {
|
||||
const fetchFn = useCallback(
|
||||
(page: number, size: number) => getArticleGroupPage({ page, size }),
|
||||
[],
|
||||
)
|
||||
|
||||
const table = useDataTable(fetchFn)
|
||||
const [dialogOpen, setDialogOpen] = useState(false)
|
||||
const [editing, setEditing] = useState<ArticleGroup | null>(null)
|
||||
const [code, setCode] = useState("")
|
||||
const [name, setName] = useState("")
|
||||
const [sort, setSort] = useState("0")
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
const openCreate = () => {
|
||||
setEditing(null)
|
||||
setCode("")
|
||||
setName("")
|
||||
setSort("0")
|
||||
setDialogOpen(true)
|
||||
}
|
||||
|
||||
const openEdit = (group: ArticleGroup) => {
|
||||
setEditing(group)
|
||||
setCode(group.code)
|
||||
setName(group.name)
|
||||
setSort(String(group.sort))
|
||||
setDialogOpen(true)
|
||||
}
|
||||
|
||||
const handleSave = useCallback(async () => {
|
||||
if (!code.trim() || !name.trim()) return
|
||||
setSaving(true)
|
||||
try {
|
||||
if (editing) {
|
||||
const resp = await updateArticleGroup({
|
||||
id: editing.id,
|
||||
code: code.trim(),
|
||||
name: name.trim(),
|
||||
sort: Number(sort),
|
||||
})
|
||||
if (resp.success) {
|
||||
toast.success("分组更新成功")
|
||||
setDialogOpen(false)
|
||||
table.refresh()
|
||||
} else {
|
||||
toast.error(resp.message || "更新失败")
|
||||
}
|
||||
} else {
|
||||
const resp = await createArticleGroup({
|
||||
code: code.trim(),
|
||||
name: name.trim(),
|
||||
sort: Number(sort),
|
||||
})
|
||||
if (resp.success) {
|
||||
toast.success("分组创建成功")
|
||||
setDialogOpen(false)
|
||||
table.refresh()
|
||||
} else {
|
||||
toast.error(resp.message || "创建失败")
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}, [code, name, sort, editing, table])
|
||||
|
||||
const handleDelete = useCallback(
|
||||
async (id: number) => {
|
||||
if (!confirm("确定要删除该分组吗?")) return
|
||||
const resp = await removeArticleGroup(id)
|
||||
if (resp.success) {
|
||||
toast.success("分组已删除")
|
||||
table.refresh()
|
||||
} else {
|
||||
toast.error(resp.message || "删除失败")
|
||||
}
|
||||
},
|
||||
[table],
|
||||
)
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-bold">文章分组</h1>
|
||||
<Button onClick={openCreate}>新建分组</Button>
|
||||
</div>
|
||||
|
||||
<Suspense>
|
||||
<DataTable<ArticleGroup>
|
||||
{...table}
|
||||
columns={[
|
||||
{ header: "编码", accessorKey: "code" },
|
||||
{ header: "名称", accessorKey: "name" },
|
||||
{ header: "排序", accessorKey: "sort" },
|
||||
{
|
||||
header: "创建时间",
|
||||
accessorKey: "created_at",
|
||||
cell: ({ row }) => formatDate(row.original.created_at),
|
||||
},
|
||||
{
|
||||
header: "操作",
|
||||
id: "actions",
|
||||
cell: ({ row }) => (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
onClick={() => openEdit(row.original)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
className="text-destructive"
|
||||
onClick={() => handleDelete(row.original.id)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Suspense>
|
||||
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editing ? "编辑分组" : "新建分组"}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>编码</Label>
|
||||
<Input
|
||||
value={code}
|
||||
onChange={e => setCode(e.target.value)}
|
||||
placeholder="分组编码,如 news"
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>名称</Label>
|
||||
<Input
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
placeholder="分组名称"
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>排序</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={sort}
|
||||
onChange={e => setSort(e.target.value)}
|
||||
placeholder="0"
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setDialogOpen(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={saving}>
|
||||
{saving ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
保存中...
|
||||
</>
|
||||
) : (
|
||||
"保存"
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
135
src/app/(root)/articles/[id]/article-editor.tsx
Normal file
135
src/app/(root)/articles/[id]/article-editor.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
"use client"
|
||||
|
||||
import { Loader2 } from "lucide-react"
|
||||
import dynamic from "next/dynamic"
|
||||
import Link from "next/link"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
import { toast } from "sonner"
|
||||
import { createArticle, getArticle, updateArticle } from "@/actions/article"
|
||||
import type { ArticleGroup } from "@/models/article-group"
|
||||
|
||||
const Editor = dynamic(() => import("@/components/editor/richTextEditor"), {
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
<div className="flex items-center justify-center h-64 text-muted-foreground">
|
||||
编辑器加载中...
|
||||
</div>
|
||||
),
|
||||
})
|
||||
|
||||
interface ArticleEditorProps {
|
||||
articleId: string
|
||||
groups: ArticleGroup[]
|
||||
}
|
||||
|
||||
export default function ArticleEditor({
|
||||
articleId,
|
||||
groups,
|
||||
}: ArticleEditorProps) {
|
||||
const router = useRouter()
|
||||
const isNew = articleId === "new"
|
||||
const [loading, setLoading] = useState(!isNew)
|
||||
const [title, setTitle] = useState("")
|
||||
const [initialContent, setInitialContent] = useState("")
|
||||
const [groupId, setGroupId] = useState<string>(
|
||||
groups.length > 0 ? String(groups[0].id) : "",
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isNew) {
|
||||
const id = Number(articleId)
|
||||
if (Number.isNaN(id)) {
|
||||
toast.error("无效的文章 ID")
|
||||
router.push("/articles")
|
||||
return
|
||||
}
|
||||
getArticle(id).then(resp => {
|
||||
if (resp.success && resp.data) {
|
||||
setTitle(resp.data.title || "")
|
||||
setInitialContent(resp.data.content || "")
|
||||
if (resp.data.group_id) {
|
||||
setGroupId(String(resp.data.group_id))
|
||||
}
|
||||
} else {
|
||||
toast.error(
|
||||
resp.success ? "文章数据为空" : resp.message || "文章不存在",
|
||||
)
|
||||
router.push("/articles")
|
||||
}
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
}, [articleId, isNew, router])
|
||||
|
||||
const handleSave = useCallback(
|
||||
async (data: { title: string; content: string }) => {
|
||||
if (!groupId) {
|
||||
toast.error("请选择文章分组")
|
||||
return
|
||||
}
|
||||
if (isNew) {
|
||||
const resp = await createArticle({
|
||||
title: data.title,
|
||||
content: data.content,
|
||||
group_id: Number(groupId),
|
||||
status: 1,
|
||||
})
|
||||
if (resp.success) {
|
||||
toast.success("文章创建成功")
|
||||
router.push("/articles")
|
||||
} else {
|
||||
toast.error(resp.message || "创建失败")
|
||||
}
|
||||
} else {
|
||||
const id = Number(articleId)
|
||||
const resp = await updateArticle({
|
||||
id,
|
||||
title: data.title,
|
||||
content: data.content,
|
||||
group_id: Number(groupId),
|
||||
status: 1,
|
||||
})
|
||||
if (resp.success) {
|
||||
toast.success("文章保存成功")
|
||||
router.push("/articles")
|
||||
} else {
|
||||
toast.error(resp.message || "保存失败")
|
||||
}
|
||||
}
|
||||
},
|
||||
[isNew, articleId, groupId, router],
|
||||
)
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (groups.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full gap-4 text-muted-foreground">
|
||||
<p>还没有文章分组,请先创建分组</p>
|
||||
<Link href="/article-groups">
|
||||
<span className="text-primary hover:underline">前往创建分组</span>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 h-full min-h-0">
|
||||
<Editor
|
||||
content={initialContent}
|
||||
title={title}
|
||||
groups={groups}
|
||||
groupId={groupId}
|
||||
onGroupChange={setGroupId}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
22
src/app/(root)/articles/[id]/page.tsx
Normal file
22
src/app/(root)/articles/[id]/page.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { getArticleGroupList } from "@/actions/article-group"
|
||||
import { Page } from "@/components/page"
|
||||
import ArticleEditor from "./article-editor"
|
||||
|
||||
export default async function ArticleEditPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>
|
||||
}) {
|
||||
const { id } = await params
|
||||
const groupsResp = await getArticleGroupList()
|
||||
console.log(groupsResp, "groupsResp")
|
||||
|
||||
const groups = groupsResp.success ? groupsResp.data : []
|
||||
console.log(groups, "groups")
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<ArticleEditor articleId={id} groups={groups} />
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
105
src/app/(root)/articles/page.tsx
Normal file
105
src/app/(root)/articles/page.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
"use client"
|
||||
|
||||
import Link from "next/link"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Suspense, useCallback } from "react"
|
||||
import { toast } from "sonner"
|
||||
import { getArticlePage, removeArticle } from "@/actions/article"
|
||||
import { DataTable, useDataTable } from "@/components/data-table"
|
||||
import { Page } from "@/components/page"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import type { Article } from "@/models/article"
|
||||
import { formatDate } from "@/models/formatDate"
|
||||
|
||||
export default function ArticleListPage() {
|
||||
const router = useRouter()
|
||||
const fetchFn = useCallback(
|
||||
(page: number, size: number) => getArticlePage({ page, size }),
|
||||
[],
|
||||
)
|
||||
|
||||
const table = useDataTable(fetchFn)
|
||||
console.log(table, "tabletabletable")
|
||||
|
||||
const handleDelete = useCallback(
|
||||
async (id: number, title: string) => {
|
||||
if (!confirm(`确定要删除文章《${title}》吗?此操作不可恢复!`)) return
|
||||
|
||||
try {
|
||||
const resp = await removeArticle(id)
|
||||
console.log(resp, "resprespresp")
|
||||
|
||||
if (resp.success) {
|
||||
toast.success("文章已删除")
|
||||
table.refresh()
|
||||
router.refresh()
|
||||
} else {
|
||||
toast.error(resp.message || "删除失败")
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("删除失败")
|
||||
console.error(error)
|
||||
}
|
||||
},
|
||||
[table, router],
|
||||
)
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-bold">文章管理</h1>
|
||||
<Link href="/articles/new">
|
||||
<Button>新建文章</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<Suspense>
|
||||
<DataTable<Article>
|
||||
{...table}
|
||||
columns={[
|
||||
{ header: "标题", accessorKey: "title" },
|
||||
{
|
||||
header: "状态",
|
||||
accessorKey: "status",
|
||||
cell: ({ row }) => (
|
||||
<Badge
|
||||
variant={row.original.status === 1 ? "default" : "secondary"}
|
||||
>
|
||||
{row.original.status === 1 ? "已发布" : "草稿"}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "更新时间",
|
||||
accessorKey: "updated_at",
|
||||
cell: ({ row }) => formatDate(row.original.updated_at),
|
||||
},
|
||||
{
|
||||
header: "操作",
|
||||
id: "actions",
|
||||
cell: ({ row }) => (
|
||||
<div className="flex gap-2">
|
||||
<Link
|
||||
href={`/articles/${row.original.id}`}
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
编辑
|
||||
</Link>
|
||||
<button
|
||||
onClick={() =>
|
||||
handleDelete(row.original.id, row.original.title)
|
||||
}
|
||||
className="text-destructive hover:underline"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Suspense>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client"
|
||||
import { useSetAtom } from "jotai"
|
||||
import { useEffect } from "react"
|
||||
import { scopesAtom } from "@/lib/stores/scopes"
|
||||
import type { Admin } from "@/models/admin"
|
||||
|
||||
@@ -8,7 +9,9 @@ export default function SetScopes(props: {
|
||||
}) {
|
||||
const setScopes = useSetAtom(scopesAtom)
|
||||
|
||||
console.log("用户权限", props.admin.scopes)
|
||||
setScopes(props.admin.scopes)
|
||||
useEffect(() => {
|
||||
setScopes(props.admin.scopes)
|
||||
}, [props.admin.scopes, setScopes])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -122,3 +122,127 @@
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
.tiptap-editor {
|
||||
.ProseMirror {
|
||||
@apply w-full outline-none p-4;
|
||||
|
||||
h1 {
|
||||
@apply text-2xl font-bold mt-4 mb-2;
|
||||
}
|
||||
h2 {
|
||||
@apply text-xl font-semibold mt-3 mb-2;
|
||||
}
|
||||
h3 {
|
||||
@apply text-lg font-medium mt-2 mb-1;
|
||||
}
|
||||
p {
|
||||
@apply my-2 leading-7;
|
||||
}
|
||||
ul {
|
||||
@apply list-disc pl-6 my-2;
|
||||
}
|
||||
ol {
|
||||
@apply list-decimal pl-6 my-2;
|
||||
}
|
||||
li {
|
||||
@apply my-1;
|
||||
}
|
||||
blockquote {
|
||||
@apply border-l-4 border-muted pl-4 italic text-muted-foreground my-3;
|
||||
}
|
||||
code {
|
||||
@apply bg-muted px-1.5 py-0.5 rounded text-sm font-mono text-rose-600;
|
||||
}
|
||||
pre {
|
||||
@apply bg-slate-900 text-slate-100 rounded-lg p-4 my-3 overflow-x-auto;
|
||||
code {
|
||||
@apply bg-transparent p-0 text-sm text-inherit;
|
||||
}
|
||||
}
|
||||
hr {
|
||||
@apply my-4 border-border;
|
||||
}
|
||||
a {
|
||||
@apply text-blue-600 underline;
|
||||
}
|
||||
strong {
|
||||
@apply font-semibold;
|
||||
}
|
||||
em {
|
||||
@apply italic;
|
||||
}
|
||||
s {
|
||||
@apply line-through;
|
||||
}
|
||||
ul[data-type="taskList"] {
|
||||
@apply list-none pl-0;
|
||||
li {
|
||||
@apply flex items-start gap-2 my-1;
|
||||
label {
|
||||
@apply mt-1;
|
||||
}
|
||||
div {
|
||||
@apply flex-1;
|
||||
}
|
||||
}
|
||||
}
|
||||
table {
|
||||
@apply border-collapse w-full my-3;
|
||||
th,
|
||||
td {
|
||||
@apply border border-border px-3 py-2 text-sm;
|
||||
}
|
||||
th {
|
||||
@apply bg-muted font-semibold text-left;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Code block syntax highlighting */
|
||||
.tiptap-editor pre code .hljs-keyword,
|
||||
.tiptap-editor pre code .hljs-selector-tag,
|
||||
.tiptap-editor pre code .hljs-type {
|
||||
color: #c792ea;
|
||||
}
|
||||
|
||||
.tiptap-editor pre code .hljs-string,
|
||||
.tiptap-editor pre code .hljs-attr {
|
||||
color: #c3e88d;
|
||||
}
|
||||
|
||||
.tiptap-editor pre code .hljs-number,
|
||||
.tiptap-editor pre code .hljs-literal {
|
||||
color: #f78c6c;
|
||||
}
|
||||
|
||||
.tiptap-editor pre code .hljs-comment {
|
||||
color: #676e95;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.tiptap-editor pre code .hljs-title,
|
||||
.tiptap-editor pre code .hljs-function,
|
||||
.tiptap-editor pre code .hljs-name {
|
||||
color: #82aaff;
|
||||
}
|
||||
|
||||
.tiptap-editor pre code .hljs-built_in,
|
||||
.tiptap-editor pre code .hljs-symbol {
|
||||
color: #ffcb6b;
|
||||
}
|
||||
|
||||
.tiptap-editor pre code .hljs-variable,
|
||||
.tiptap-editor pre code .hljs-params {
|
||||
color: #f07178;
|
||||
}
|
||||
|
||||
.tiptap-editor pre code .hljs-meta,
|
||||
.tiptap-editor pre code .hljs-tag {
|
||||
color: #89ddff;
|
||||
}
|
||||
|
||||
.tiptap-editor pre code .hljs-attr {
|
||||
color: #c3e88d;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user