【山竹记账前端-react】21.标签页面


大纲链接 §

[toc]


1. 新建标签页 ⇧

添加路由 @/route/routes.tsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { Root } from '@/components/Root'
import { Home } from '@/pages/Home'
import { ItemCreatePage } from '@/pages/ItemCreatePage'
import { ItemsPage } from '@/pages/ItemsPage'
import { SignInPage } from '@/pages/SignInPage'
import { TagCreatePage } from '@/pages/TagCreatePage'
import { welcomeRouter } from '@/route/welcomeRouter'
import type { RouteObject } from 'react-router-dom'

export const routes: RouteObject[] = [
  { path: '/', element: <Root /> },
  welcomeRouter,
  { path: '/home', element: <Home title="首页" /> },
  { path: '/sign_in', element: <SignInPage /> },
  { path: '/items', element: <ItemsPage /> },
  { path: '/items/create', element: <ItemCreatePage /> },
  { path: '/tags/create', element: <TagCreatePage /> },
  { path: '/tags', element: <div>标签</div> },
  { path: '/statistics', element: <div>统计图表</div> },
  { path: '/export', element: <div>敬请期待</div> },
  { path: '/notify', element: <div>敬请期待</div> },
  { path: '/tech_stack', element: <div>敬请期待</div> },
]

@/pages/TagCreatePage.tsx

1
2
3
4
5
6
7
interface Props {
  className?: string
}

export const TagCreatePage: React.FC<Props> = ({/* className, */}) => {
  return <div>TagCreatePage</div>
}

标签列表组件跳转逻辑 @/modules/tags/IconTags.tsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import { SvgIcon } from '@/components/SvgIcon'
import type { TabKind } from '@/global'
import { IconTagItem } from '@/modules/tags/IconTagItem'
import { cn } from 'cn'
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'

interface Props {
  className?: string
  kind: TabKind
}

const tags = Array.from({ length: 98 }, (_, i) => i + 1)

export const IconTags: React.FC<Props> = ({ className, kind }) => {
  console.log('IconTags %c⧭ kind', 'color: #007300', kind)

  const [selectedTabKey, setSelectedTabKey] = useState<string>('1')

  const navigate = useNavigate()
  const toCreateTagPage = () => {
    navigate(`/tags/create?kind=${kind}`)
  }
  return (
    <div className={cn(className)}>
      <ol
        className={cn(
          'grid place-items-center justify-center grid-cols-[repeat(auto-fit,3em)]',
          'py-1em px-.5em gap-x-[2em] gap-y-[1em]'
        )}>
        <IconTagItem tagKey="add" tagName="添加" onClick={toCreateTagPage}>
          <SvgIcon name="add" className="size-[24px] fill-[var(--main)]" />
        </IconTagItem>
        {tags.map((tag) => (
          <IconTagItem
            key={tag}
            selectedKey={selectedTabKey}
            onSelectKey={setSelectedTabKey}
            tagKey={`${tag}`}
            tagName="打的"
            tagSign="🤑"
          />
        ))}
      </ol>
    </div>
  )
}

内部标签组建添加点击逻辑 @/modules/tags/IconTagItem.tsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import { cn } from 'cn'
import React from 'react'

interface Props {
  className?: string
  tagKey: string
  tagName: string
  selectedKey?: string
  tagSign?: string
  children?: React.ReactNode
  onSelectKey?: (tagKey: string) => void
  onClick?: () => void
}

export const IconTagItem: React.FC<Props> = ({
  className,
  tagKey,
  tagName,
  tagSign,
  children,
  onSelectKey,
  selectedKey,
  onClick,
}) => {
  const onClickTagItem = () => {
    onSelectKey?.(tagKey)
    onClick?.()
  }
  return (
    <li
      onClick={onClickTagItem}
      className={cn(className, 'w-3em flex flex-col items-center justify-center gap-y-.5em')}>
      <span
        className={cn(
          'flex items-center justify-center',
          'where:(size-[48px] text-[24px] rounded-[50%])',
          'where:(b-1 bg-[var(--item-tag-bg)])',
          selectedKey === tagKey ? 'b-[var(--main)] b-1.5' : 'b-[var(--main)]/20'
        )}>
        {tagSign ?? children}
      </span>
      <span
        className={cn(
          'text-12px ',
          selectedKey === tagKey
            ? 'color-[var(--main)] font-bold'
            : 'color-[var(--item-tag-text-color)]'
        )}>
        {tagName}
      </span>
    </li>
  )
}

2. 新建标签页-搭出框架 ⇧

@/pages/TagCreatePage.tsx 完成初步结构样式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import type { FormEventHandler } from 'react'
import { cn } from 'cn';
import { GradientBanner } from '@/components/GradientBanner';
import { TopNav } from '@/components/TopNav';
import { SvgIcon } from '@/components/SvgIcon';

interface Props {
  className?: string
}

export const TagCreatePage: React.FC<Props> = ({/* className, */ }) => {
  const Icon = <SvgIcon name="back-arrow" className="" />
  const onSubmit: FormEventHandler<HTMLFormElement> = (e) => {
    e.preventDefault()
  }

  return (
    <div className={cn('container w-screen h-screen relative')}>
      <GradientBanner>
        <TopNav title="新建标签" icon={Icon} />
      </GradientBanner>

      <div className="px-[1em]">
        <form onSubmit={onSubmit}>
          <div>
            <span>标签名</span>
            <input className="form-input-text-sc" />
            <span>标签名太长</span>
          </div>

          <div>
            <span>符号 😄</span>
            {/* <EmojiSelector /> */}
          </div>

          <p>记账时长按即可进行编辑</p>

          <div>
            <button type="submit" className="primary-btn">
              确定
            </button>
          </div>
        </form>
      </div>
    </div>
  )
}

3. 选看:整理 Emoji ⇧

准备好已爬去的 emoji 列表数据 ⇧

@/lib/const/safeEmojiList.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// 数据来源: https://unicode.org/emoji/charts/full-emoji-list.html
// index=页面序号, unicode=\u{...} 形式(运行时常量即渲染 emoji),
// emoji=真实emoji字符(来自 full-emoji-list.json 的 unicode 字段), unicode_raw=原始码位, name=CLDR short name
// 注意: \u{...} 为 ES6 转义, 需 tsconfig target >= ES2015
import type { EmojiSubTopic } from '@/lib/const/emojiTopicList.ts'

export interface EmojiItem {
  index: number;
  unicode: string;
  emoji: string;
  unicode_raw: string;
  hint: string;
}

export interface EmojiTopic {
  topic: EmojiSubTopic;
  emoji_list: EmojiItem[];
}

/**
 * @Description: 颜文字列表
 * @Description: Full Emoji List, v18.0 https://unicode.org/emoji/charts/full-emoji-list.html
 */
export const safeEmojiList: Readonly<EmojiTopic[]> = Object.freeze([
  {
    topic: "face-smiling",
    emoji_list: [
      { index: 1, unicode: "\u{1F600}", emoji: "😀", unicode_raw: "U+1F600", hint: "grinning face" },
      { index: 2, unicode: "\u{1F603}", emoji: "😃", unicode_raw: "U+1F603", hint: "grinning face with big eyes" },
      { index: 3, unicode: "\u{1F604}", emoji: "😄", unicode_raw: "U+1F604", hint: "grinning face with smiling eyes" },
      { index: 4, unicode: "\u{1F601}", emoji: "😁", unicode_raw: "U+1F601", hint: "beaming face with smiling eyes" },
      { index: 5, unicode: "\u{1F606}", emoji: "😆", unicode_raw: "U+1F606", hint: "grinning squinting face" },
      { index: 6, unicode: "\u{1F605}", emoji: "😅", unicode_raw: "U+1F605", hint: "grinning face with sweat" },
      { index: 7, unicode: "\u{1F923}", emoji: "🤣", unicode_raw: "U+1F923", hint: "rolling on the floor laughing" },
      { index: 8, unicode: "\u{1F602}", emoji: "😂", unicode_raw: "U+1F602", hint: "face with tears of joy" },
      { index: 9, unicode: "\u{1F642}", emoji: "🙂", unicode_raw: "U+1F642", hint: "slightly smiling face" },
      { index: 10, unicode: "\u{1F643}", emoji: "🙃", unicode_raw: "U+1F643", hint: "upside-down face" },
      { index: 11, unicode: "\u{1FAE0}", emoji: "🫠", unicode_raw: "U+1FAE0", hint: "melting face" },
      { index: 12, unicode: "\u{1FAEB}", emoji: "🫫", unicode_raw: "U+1FAEB", hint: "⊛ cracking face" },
      { index: 13, unicode: "\u{1F609}", emoji: "😉", unicode_raw: "U+1F609", hint: "winking face" },
      { index: 14, unicode: "\u{1F60A}", emoji: "😊", unicode_raw: "U+1F60A", hint: "smiling face with smiling eyes" },
      { index: 15, unicode: "\u{1F607}", emoji: "😇", unicode_raw: "U+1F607", hint: "smiling face with halo" }
    ],
  },
  {
    topic: "face-affection",
    emoji_list: [
      { index: 16, unicode: "\u{1F970}", emoji: "🥰", unicode_raw: "U+1F970", hint: "smiling face with hearts" },
      { index: 17, unicode: "\u{1F60D}", emoji: "😍", unicode_raw: "U+1F60D", hint: "smiling face with heart-eyes" },
      { index: 18, unicode: "\u{1F929}", emoji: "🤩", unicode_raw: "U+1F929", hint: "star-struck" },
      { index: 19, unicode: "\u{1F618}", emoji: "😘", unicode_raw: "U+1F618", hint: "face blowing a kiss" },
      { index: 20, unicode: "\u{1F617}", emoji: "😗", unicode_raw: "U+1F617", hint: "kissing face" },
      { index: 21, unicode: "\u{263A}", emoji: "☺", unicode_raw: "U+263A", hint: "smiling face" },
      { index: 22, unicode: "\u{1F61A}", emoji: "😚", unicode_raw: "U+1F61A", hint: "kissing face with closed eyes" },
      { index: 23, unicode: "\u{1F619}", emoji: "😙", unicode_raw: "U+1F619", hint: "kissing face with smiling eyes" },
      { index: 24, unicode: "\u{1F972}", emoji: "🥲", unicode_raw: "U+1F972", hint: "smiling face with tear" }
    ],
  },
  {
    topic: "face-tongue",
    emoji_list: [
      { index: 25, unicode: "\u{1F60B}", emoji: "😋", unicode_raw: "U+1F60B", hint: "face savoring food" },
      { index: 26, unicode: "\u{1F61B}", emoji: "😛", unicode_raw: "U+1F61B", hint: "face with tongue" },
      { index: 27, unicode: "\u{1F61C}", emoji: "😜", unicode_raw: "U+1F61C", hint: "winking face with tongue" },
      { index: 28, unicode: "\u{1F92A}", emoji: "🤪", unicode_raw: "U+1F92A", hint: "zany face" },
      { index: 29, unicode: "\u{1F61D}", emoji: "😝", unicode_raw: "U+1F61D", hint: "squinting face with tongue" },
      { index: 30, unicode: "\u{1F911}", emoji: "🤑", unicode_raw: "U+1F911", hint: "money-mouth face" }
    ],
  },
  {
    topic: "face-hand",
    emoji_list: [
      { index: 31, unicode: "\u{1F917}", emoji: "🤗", unicode_raw: "U+1F917", hint: "smiling face with open hands" },
      { index: 32, unicode: "\u{1F92D}", emoji: "🤭", unicode_raw: "U+1F92D", hint: "face with hand over mouth" },
      { index: 33, unicode: "\u{1FAE2}", emoji: "🫢", unicode_raw: "U+1FAE2", hint: "face with open eyes and hand over mouth" },
      { index: 34, unicode: "\u{1FAE3}", emoji: "🫣", unicode_raw: "U+1FAE3", hint: "face with peeking eye" },
      { index: 35, unicode: "\u{1F92B}", emoji: "🤫", unicode_raw: "U+1F92B", hint: "shushing face" },
      { index: 36, unicode: "\u{1F914}", emoji: "🤔", unicode_raw: "U+1F914", hint: "thinking face" },
      { index: 37, unicode: "\u{1FAE1}", emoji: "🫡", unicode_raw: "U+1FAE1", hint: "saluting face" }
    ],
  },
  // ...
] as const)

@/lib/const/emojiTopicList.ts

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
export const emojiTopicList = Object.freeze([
  {
    mainTopic: '表情',
    topics: [
      'face-smiling',
      'face-affection',
      'face-tongue',
      'face-hand',
      'face-neutral-skeptical',
      'face-sleepy',
      'face-unwell',
      'face-hat',
      'face-glasses',
      'face-concerned',
      'face-negative',
      'face-costume',
      'cat-face',
      'monkey-face',
      'heart',
      'emotion',
    ],
  },
  {
    mainTopic: '手势',
    topics: [
      'hand-fingers-open',
      'hand-fingers-partial',
      'hand-single-finger',
      'hand-fingers-closed',
      'hands',
      'hand-prop',
    ],
  },
  {
    mainTopic: '食物',
    topics: [
      'food-fruit',
      'food-vegetable',
      'food-prepared',
      'food-asian',
      'food-sweet',
      'drink',
      'dishware',
    ],
  },
  {
    mainTopic: '动物',
    topics: [
      'animal-mammal',
      'animal-bird',
      'animal-amphibian',
      'animal-reptile',
      'animal-marine',
      'animal-bug',
    ],
  },
  {
    mainTopic: '植物',
    topics: ['plant-flower', 'plant-other'],
  },
  {
    mainTopic: '自然',
    topics: ['time', 'sky & weather'],
  },
  {
    mainTopic: '人物',
    topics: [
      'person',
      'person-gesture',
      'person-fantasy',
      'person-activity',
      'person-resting',
      'family',
      'person-symbol',
    ],
  },
  {
    mainTopic: '场所',
    topics: [
      'place-map',
      'place-geographic',
      'place-building',
      'place-religious',
      'place-other',
      'hotel',
    ],
  },
  {
    mainTopic: '交通',
    topics: ['transport-ground', 'transport-water', 'transport-air', 'transport-sign', 'warning'],
  },
  {
    mainTopic: '节日',
    topics: ['event'],
  },
  {
    mainTopic: '运动',
    topics: ['person-sport', 'sport', 'award-medal'],
  },
  {
    mainTopic: '衣服',
    topics: ['clothing'],
  },
  {
    mainTopic: '文艺',
    topics: [
      'arts & crafts',
      'game',
      'music',
      'musical-instrument',
      'sound',
      'light & video',
      'book-paper',
      'writing',
    ],
  },
  {
    mainTopic: '职业',
    topics: [
      'person-role',
      'money',
      'currency',
      'phone',
      'computer',
      'mail',
      'office',
      'lock',
      'tool',
      'household',
    ],
  },
  {
    mainTopic: '科技',
    topics: ['science', 'medical', 'math'],
  },
  {
    mainTopic: '个人',
    topics: ['body-parts', 'hair-style', 'religion', 'zodiac', 'gender'],
  },
  {
    mainTopic: '其他',
    topics: [
      'other-object',
      'arrow',
      'av-symbol',
      'punctuation',
      'other-symbol',
      'alphanum',
      'flag',
    ],
  },
] as const)

export type EmojiMainTopic = (typeof emojiTopicList)[number]['mainTopic']
export type EmojiTopics = (typeof emojiTopicList)[number]['topics']
export type EmojiSubTopic = EmojiTopics[number]

封装颜文字选择器 ⇧

  • 颜文字选择器 EmojiSelector 主要分为上下两部分
    • 导航部分 EmojiMainTopicNav 可横向切换选择颜文字主题
    • 内容部分 EmojiUnicodeList 可选择颜文字图标

@/modules/tags/emoji/EmojiSelector.tsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/*
 * @Author: xmasuhai xmasuhai@163.com
 * @Date: 2026-11-11 19:16:37
 * @LastEditors: xmasuhai xmasuhai@163.com
 * @LastEditTime: 2026-11-11 23:14:52
 * @FilePath: /mangosteen-react-1/src/modules/tags/emoji/EmojiSelector.tsx
 * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
 */
import { useState } from 'react'
import { cn } from 'cn'
import { EmojiMainTopicNav } from '@/modules/tags/emoji/EmojiMainTopicNav'
import { EmojiUnicodeList } from '@/modules/tags/emoji/EmojiUnicodeList'
import type { EmojiMainTopic } from '@/lib/const/emojiTopicList';

interface Props {
  className?: string
  hasError?: boolean
}

export const EmojiSelector: React.FC<Props> = ({ className, hasError }) => {
  const [_selectedEmoji, _setSelectedEmoji] = useState('')
  const [mainTopic, setMainTopic] = useState<EmojiMainTopic>('表情')
  const onChangeMainTopic = (topic: EmojiMainTopic) => {
    setMainTopic(topic)
  }

  return (
    <div
      className={cn(
        'min-h-[var(--input-min-height)] max-w-full flex-grow-1',
        'b-1 rounded-[var(--input-radius)] text-14px',
        hasError ? 'b-[var(--error-color)]' : 'b-[var(--input-border-color)]',
        className
      )}>
      <EmojiMainTopicNav onChangeMainTopic={onChangeMainTopic } />

      <EmojiUnicodeList mainTopic={mainTopic} />
    </div>
  )
}

@/modules/tags/emoji/EmojiMainTopicNav.tsx

  • 外部接受参数 mainTopic
  • 父组件传参的状态修改方法 onChangeMainTopic
  • 点击项目,更新外部状态

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    
    import { useMemo, useState } from 'react'
    import { type EmojiMainTopic, emojiTopicList } from '@/lib/const/emojiTopicList.ts'
    import { cn } from 'cn'
    
    interface Props {
    className?: string
    mainTopic?: string
    onChangeMainTopic: (topic: EmojiMainTopic) => void
    }
    
    export const EmojiMainTopicNav: React.FC<Props> = ({ className, mainTopic = '表情', onChangeMainTopic }) => {
    const navList = useMemo(() => emojiTopicList.map((topicItem) => topicItem.mainTopic), [])
    const [selectedMainTopic, setSelectedMainTopic] = useState(navList?.[0] ?? mainTopic)
    const selectMainTopic = (topic: EmojiMainTopic) => {
    setSelectedMainTopic(topic)
    onChangeMainTopic(topic)
    }
    
    return (
    <nav className={className}>
      <menu
        className={cn(
          'flex flex-nowrap overflow-auto mx-1em py-0 scrollbar-hide',
          '[&>li.selected]:(text-[var(--emoji-nav-text-selected)] font-bold)'
        )}>
        {navList.map((item, index) => (
          <li
            key={`${index}_${item}`}
            onClick={() => selectMainTopic(item)}
            className={cn(
              'shrink-0 inline-block p-.5em text-[var(--emoji-nav-text)] whitespace-nowrap',
              item === selectedMainTopic ? 'selected' : ''
            )}>
            {item}
          </li>
        ))}
      </menu>
    </nav>
    )
    }
    

@/modules/tags/emoji/EmojiUnicodeList.tsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import { cn } from 'cn'
import { useMemo } from 'react'
import {
  type EmojiMainTopic,
  type EmojiSubTopic,
  emojiTopicList,
} from '@/lib/const/emojiTopicList.ts'
import { safeEmojiList, type EmojiItem } from '@/lib/const/safeEmojiList.ts'

interface Props {
  className?: string
  mainTopic?: EmojiMainTopic
}

export const EmojiUnicodeList: React.FC<Props> = ({ className, mainTopic = '表情' }) => {
  const selectedTopicList = useMemo<readonly EmojiSubTopic[]>(
    () =>
      emojiTopicList?.find((topicItem) => topicItem.mainTopic === mainTopic)?.topics ?? [],
    [mainTopic]
  )

  const selectedFlattedEmojiList = useMemo<EmojiItem[]>(
    () =>
      safeEmojiList
        .filter((item) => selectedTopicList.includes(item.topic))
        .flatMap((item) => item.emoji_list),
    [selectedTopicList]
  )

  // ["\u{1F600}", ...]
  const selectedMainTopicEmojiUnicodeList = useMemo<string[]>(
    () => selectedFlattedEmojiList.map((item) => item.unicode),
    [selectedFlattedEmojiList]
  )

  return (
    <ol
      className={cn(
        'scrollbar-hide overflow-auto h-[calc(1.65em_*_12)]',
        'flex flex-wrap content-start px-.5em py-2.5px',
        'text-22px line-height-1.5em',
        'children:(text-22px)',
        className
      )}>
      {selectedMainTopicEmojiUnicodeList.map((unicode, index) => (
        <li
          key={`${index}_${unicode}`}
          onClick={() => {
            /* switchEmojiUnicode(unicode) */
          }}
          className={cn(
            'text-center grow-0 shrink-0 w-10% b-1 b-transparent'
            // unicode === selectedEmojiUnicode.value &&
            // 'bg-blue-300 outline-1px outline-solid outline-[var(--main)] rounded-[var(--input-radius)]',
          )}>
          {unicode}
        </li>
      ))}
    </ol>
  )
}

4. 新建标签页面-完成表单大致结构样式 ⇧


5. $3 ⇧


6. $3 ⇧


·未完待续·

参考文章

  • 无

相关文章

  • 无

  • 作者: Joel
  • 文章链接:
  • 版权声明
  • 非自由转载-非商用-非衍生-保持署名