소스 검색

feat(标签管理): 新增标签编辑功能及相关组件

- 添加 TagCardEdit 组件用于编辑新标签
- 扩展 TagCloud 组件支持添加和编辑新标签
- 在 PostEditView 中集成标签选择和管理功能
- 新增 NewTag 接口定义和关闭图标
- 调整相关样式和交互效果
Sakulin 2 달 전
부모
커밋
53a1466b62
8개의 변경된 파일224개의 추가작업 그리고 39개의 파일을 삭제
  1. 8 0
      src/assets/icons.ts
  2. 15 1
      src/assets/main.css
  3. 2 0
      src/components/TagCard.vue
  4. 84 0
      src/components/TagCardEdit.vue
  5. 51 2
      src/components/TagCloud.vue
  6. 5 0
      src/models/index.ts
  7. 59 35
      src/views/PostEditView.vue
  8. 0 1
      src/views/PostView.vue

+ 8 - 0
src/assets/icons.ts

@@ -53,3 +53,11 @@ export const writeIcon: Icon = {
   </svg>
   `
 }
+
+export const closeIcon: Icon = {
+  template: `
+  <svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
+    <path d="M240.512 180.181333l271.530667 271.488 271.530666-271.488a42.666667 42.666667 0 0 1 56.32-3.541333l4.010667 3.541333a42.666667 42.666667 0 0 1 0 60.330667l-271.530667 271.530667 271.530667 271.530666a42.666667 42.666667 0 0 1-56.32 63.872l-4.010667-3.541333-271.530666-271.530667-271.530667 271.530667-4.010667 3.541333a42.666667 42.666667 0 0 1-56.32-63.872l271.488-271.530666-271.488-271.530667a42.666667 42.666667 0 0 1 60.330667-60.330667z"/>
+  </svg>
+  `
+}

+ 15 - 1
src/assets/main.css

@@ -324,8 +324,22 @@ a {
   transition: all .3s;
 }
 
-
 .p-button:hover {
   background-size: 100% 100%;
   color: var(--background-color);
 }
+
+.p-button-success {
+  padding: 8px 12px;
+  cursor: pointer;
+  background: linear-gradient(to right, var(--success-color), var(--success-color)) no-repeat left bottom;
+  color: var(--success-color);
+  background-size: 100% 2px;
+  font-weight: bold;
+  transition: all .3s;
+}
+
+.p-button-success:hover {
+  background-size: 100% 100%;
+  color: var(--text-color);
+}

+ 2 - 0
src/components/TagCard.vue

@@ -40,6 +40,8 @@ const color = computed(() => {
   margin: 3px 6px 3px 0;
   cursor: pointer;
   transition: all .2s;
+  overflow: hidden;
+  z-index: 10;
 }
 
 .tag:hover {

+ 84 - 0
src/components/TagCardEdit.vue

@@ -0,0 +1,84 @@
+<script setup lang="ts">
+import { closeIcon } from '@/assets/icons.ts'
+
+const props = defineProps<{
+  modelValue: string,
+  onDelete?: () => void,
+}>();
+
+const emit = defineEmits(['update:modelValue']);
+
+const handleInput = (e: Event) => {
+  const target = e.target as HTMLInputElement;
+  if (target) emit('update:modelValue', target.value);
+}
+</script>
+
+<template>
+  <div class="new-tag">
+    <div style="display: flex; gap: 4px;">
+      <div style="position: relative; overflow: hidden;">
+        <input
+          :value="props.modelValue"
+          @input="handleInput"
+        />
+        <div
+          class="hidden-text"
+          :style="{
+            fontSize: '16px',
+            whiteSpace: 'nowrap'
+          }"
+        >
+        &nbsp;{{ props.modelValue }}
+        </div>
+      </div>
+      <div v-if="props.onDelete" @click="props.onDelete" class="delete-btn" v-html="closeIcon.template"/>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+
+.delete-btn {
+  width: 12px;
+  height: 12px;
+  fill: var(--danger-color)
+}
+
+input {
+  position: absolute;
+  background: transparent;
+  border: 0;
+  outline: 0;
+  min-width: 100px;
+  color: transparent;
+  font-size: 16px;
+  width: 100%;
+  height: 100%;
+  font-family: inherit;
+  transform: translate(2px, -2px);
+  caret-color: white;
+}
+
+.new-tag {
+  color: var(--secondary-text-color);
+  border: 1px solid;
+  border-radius: 50px;
+  padding: 0 8px 0 12px;
+  display: inline-block;
+  margin: 3px 6px 3px 0;
+  cursor: pointer;
+  transition: all .2s;
+  overflow: hidden;
+}
+
+.new-tag:hover {
+  text-shadow: var(--secondary-text-color) 0 0 8px;
+  box-shadow: var(--secondary-text-color) 0 0 8px;
+}
+
+.hidden-text {
+  white-space: nowrap;
+  pointer-events: none;
+}
+</style>

+ 51 - 2
src/components/TagCloud.vue

@@ -1,18 +1,47 @@
 <script setup lang="ts">
 import type { Tag } from '@/models'
-import TagCard from '@/components/TagCard.vue'
+import TagCard from './TagCard.vue'
+import TagCardEdit from './TagCardEdit.vue';
 
 const props = defineProps<{
   tags: Tag[]
   onClick?: (tag: Tag) => void
+  addable?: boolean
+  newTags?: string[]
 }>()
 
+const emit = defineEmits(['update:newTags']);
+
 function handleClick(tag: Tag) {
   if (props.onClick) {
     props.onClick(tag)
   }
 }
 
+const addTag: Tag = {
+  id: -1,
+  name: "+ 新话题",
+  color: "#88ff92"
+}
+
+const handleAddTag = () => {
+  if (props.newTags) {
+    emit("update:newTags", [...props.newTags, ""])
+  }
+}
+
+const handleUpdate = (index: number, newValue: string) => {
+  if (props.newTags) {
+    emit("update:newTags", props.newTags.map((e, i) => ((i == index) ? newValue : e)))
+  }
+}
+
+const handleDelete = (index: number) => {
+  if (props.newTags) {
+    emit("update:newTags", props.newTags.filter((_, i) => i != index))
+  }
+}
+
 </script>
 
 <template>
@@ -23,7 +52,27 @@ function handleClick(tag: Tag) {
       :target="tag"
       :on-click="() => handleClick(tag)"
     />
+    <template v-if="props.addable && props.newTags">
+      <div class="divider">&nbsp;</div>
+      <TagCardEdit
+        v-for="(tag, index) in props.newTags"
+        :key="index"
+        :model-value="tag"
+        @update:model-value="(newValue) => handleUpdate(index, newValue)"
+        :on-delete="() => handleDelete(index)"
+      />
+      <TagCard :on-click="handleAddTag" :target="addTag"/>
+    </template>
   </div>
 </template>
 
-<style scoped></style>
+<style scoped>
+.divider {
+  width: 1px;
+  background-color: transparent;
+  border-left: 1px dashed var(--secondary-text-color);
+  display: inline-block;
+  margin: 3px 12px 3px 6px;
+  overflow: hidden;
+}
+</style>

+ 5 - 0
src/models/index.ts

@@ -29,6 +29,11 @@ export interface Tag {
   color: string
 }
 
+export interface NewTag {
+  name: string
+  color: string
+}
+
 export interface TagList {
   code: number
   data: Tag[]

+ 59 - 35
src/views/PostEditView.vue

@@ -1,12 +1,49 @@
 <script setup lang="ts">
-import { ref } from 'vue'
+import { computed, onMounted, ref } from 'vue'
 import BetterSelect from '@/components/BetterSelect.vue'
+import TagCloud from '@/components/TagCloud.vue'
+import type { Tag } from '@/models'
+import { api } from '@/utils/axios.ts'
 
 const editorConfig = {
   leftToolbar: 'undo redo | image',
   rightToolbar: 'preview fullscreen',
 }
 
+const tags = ref<Tag[] | null>(null);
+
+function freshData() {
+  api.tagList().then(res => {
+    tags.value = res.data;
+  });
+}
+
+const activeTags = ref<string[]>([]);
+
+const tagsView = computed<Tag[] | null>(() => {
+  if (!tags.value) return null;
+  return tags.value.map(e => {
+    if (activeTags.value.includes(e.name)) return e;
+    else return {
+      color: "#888888",
+      id: e.id,
+      name: e.name
+    }
+  })
+});
+
+const handleSelectTag = (tag: Tag) => {
+  if (activeTags.value.includes(tag.name)) {
+    activeTags.value = activeTags.value.filter(e => e != tag.name)
+  } else {
+    activeTags.value.push(tag.name)
+  }
+}
+
+onMounted(() => {
+  freshData();
+});
+
 const title = ref('')
 
 const text = ref(`
@@ -29,6 +66,8 @@ const options = [
   {label: "其他", value: "其他"},
 ]
 
+const newTags = ref<string[]>([]);
+
 </script>
 
 <template>
@@ -49,45 +88,30 @@ const options = [
         :right-toolbar="editorConfig.rightToolbar"
       />
     </div>
+
+    <div class="post-item" style="padding: 12px 30px 24px 30px">
+      <h1>
+        这篇文章是关于什么的?做个收纳吧~
+      </h1>
+      <TagCloud v-if="tagsView" :tags="tagsView" :on-click="handleSelectTag" v-model:new-tags="newTags" :addable="true"/>
+    </div>
+
+    <div class="post-item footer" style="padding: 0 30px">
+      <div class="p-button">
+        保存草稿
+      </div>
+      <div class="p-button-success">
+        发布博文
+      </div>
+    </div>
   </div>
 </template>
 
 <style scoped>
 
-.p-select {
-  width: 64px;
-  border: none;
-  font-size: 16px;
-  margin-top: 10px;
-  margin-bottom: 10px;
-  padding: 8px;
-  background: linear-gradient(to right, var(--text-color), var(--text-color)) no-repeat left bottom;
-  background-size: 0 2px;
-  color: var(--text-color);
-  transition: all 0.3s;
-  appearance: none;
-  text-align: center;
-  -webkit-appearance: none;
-  -moz-appearance: none;
-}
-
-.p-select:hover {
-  background-size: 100% 2px;
-}
-
-.p-select option {
-  background-color: var(--background-color);
-  color: var(--text-color);
-  text-align: center;
+.footer {
+  display: flex;
+  justify-content: end;
 }
 
-.p-select option:checked {
-  background-color: var(--muted-text-color);
-}
-
-.p-select option:hover {
-  background-color: var(--muted-text-color);
-}
-
-
 </style>

+ 0 - 1
src/views/PostView.vue

@@ -83,7 +83,6 @@ function handleWriteClick() {
         <div class="float-btn-ico" v-html="writeIcon.template"/>
         <div>{{ currentTip }}</div>
       </div>
-
     </div>
     <PostCard v-for="item of data" :key="item.id" :target="item" />
     <div