Initial commit: One Pipe System
完整的管理系统,包含账户管理、卡片管理、套餐管理、财务管理等功能模块。 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
156
src/components/custom/comment-widget/index.vue
Normal file
156
src/components/custom/comment-widget/index.vue
Normal file
@@ -0,0 +1,156 @@
|
||||
<template>
|
||||
<div class="comment-module">
|
||||
<form @submit.prevent="addComment">
|
||||
<div>
|
||||
<input v-model="newComment.author" placeholder="你的名称" required />
|
||||
<textarea v-model="newComment.content" placeholder="简单说两句..." required></textarea>
|
||||
<button class="btn" type="submit">发布</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ul>
|
||||
<div class="comment-header">评论 {{ comments.length }}</div>
|
||||
<CommentItem
|
||||
class="comment-item"
|
||||
v-for="comment in comments.slice().reverse()"
|
||||
:key="comment.id"
|
||||
:comment="comment"
|
||||
:show-reply-form="showReplyForm"
|
||||
@toggle-reply="toggleReply"
|
||||
@add-reply="addReply"
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import CommentItem from './widget/CommentItem.vue'
|
||||
import { commentList, Comment } from '@/mock/temp/commentDetail'
|
||||
const comments = commentList
|
||||
|
||||
const newComment = ref<Partial<Comment>>({
|
||||
author: '',
|
||||
content: ''
|
||||
})
|
||||
|
||||
const showReplyForm = ref<number | null>(null)
|
||||
|
||||
const addComment = () => {
|
||||
if (newComment.value.author && newComment.value.content) {
|
||||
comments.value.push({
|
||||
id: Date.now(),
|
||||
author: newComment.value.author,
|
||||
content: newComment.value.content,
|
||||
timestamp: new Date().toISOString(),
|
||||
replies: []
|
||||
})
|
||||
newComment.value.author = ''
|
||||
newComment.value.content = ''
|
||||
} else {
|
||||
alert('请填写完整的评论信息')
|
||||
}
|
||||
}
|
||||
|
||||
const addReply = (commentId: number, replyAuthor: string, replyContent: string) => {
|
||||
const comment = findComment(comments.value, commentId)
|
||||
if (comment && replyAuthor && replyContent) {
|
||||
comment.replies.push({
|
||||
id: Date.now(),
|
||||
author: replyAuthor,
|
||||
content: replyContent,
|
||||
timestamp: new Date().toISOString(),
|
||||
replies: []
|
||||
})
|
||||
showReplyForm.value = null
|
||||
} else {
|
||||
alert('请填写完整的回复信息')
|
||||
}
|
||||
}
|
||||
|
||||
const toggleReply = (commentId: number) => {
|
||||
showReplyForm.value = showReplyForm.value === commentId ? null : commentId
|
||||
}
|
||||
|
||||
const findComment = (comments: Comment[], commentId: number): Comment | undefined => {
|
||||
for (const comment of comments) {
|
||||
if (comment.id === commentId) {
|
||||
return comment
|
||||
}
|
||||
const found = findComment(comment.replies, commentId)
|
||||
if (found) {
|
||||
return found
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.comment-module {
|
||||
.comment-header {
|
||||
padding-bottom: 20px;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: var(--art-gray-900);
|
||||
}
|
||||
|
||||
.comment-item {
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid var(--art-border-dashed-color);
|
||||
}
|
||||
|
||||
form {
|
||||
margin-bottom: 40px !important;
|
||||
}
|
||||
|
||||
:deep(form) {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding-bottom: 50px;
|
||||
margin: auto;
|
||||
|
||||
> div {
|
||||
input,
|
||||
textarea {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
border: 1px solid var(--art-border-dashed-color);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input {
|
||||
height: 36px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
height: 100px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: inline-block;
|
||||
width: 60px;
|
||||
height: 32px;
|
||||
margin-top: 15px;
|
||||
font-size: 14px;
|
||||
line-height: 30px;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
background-color: var(--main-color);
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
167
src/components/custom/comment-widget/widget/CommentItem.vue
Normal file
167
src/components/custom/comment-widget/widget/CommentItem.vue
Normal file
@@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<li class="comment-item">
|
||||
<div class="comment-main">
|
||||
<div class="comment-header">
|
||||
<div class="avatar" :style="{ background: randomColor() }">{{
|
||||
comment.author.substring(0, 1)
|
||||
}}</div>
|
||||
<strong class="name dark-text">{{ comment.author }}</strong>
|
||||
</div>
|
||||
<span class="content">{{ comment.content }}</span>
|
||||
<div class="comment-info">
|
||||
<span class="date">{{ formatDate(comment.timestamp) }}</span>
|
||||
<div class="btn-text" @click="toggleReply(comment.id)">回复</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="comment-replies" v-if="comment.replies.length > 0">
|
||||
<CommentItem
|
||||
v-for="reply in comment.replies"
|
||||
:key="reply.id"
|
||||
:comment="reply"
|
||||
:show-reply-form="showReplyForm"
|
||||
@toggle-reply="toggleReply"
|
||||
@add-reply="addReply"
|
||||
/>
|
||||
</ul>
|
||||
|
||||
<form v-if="showReplyForm === comment.id" @submit="handleSubmit">
|
||||
<div>
|
||||
<input v-model="replyAuthor" placeholder="你的名称" required />
|
||||
<textarea v-model="replyContent" placeholder="你的回复" required></textarea>
|
||||
<button class="btn" type="submit">发布</button>
|
||||
</div>
|
||||
</form>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AppConfig from '@/config'
|
||||
import { ref } from 'vue'
|
||||
|
||||
interface Comment {
|
||||
id: number
|
||||
author: string
|
||||
content: string
|
||||
timestamp: string
|
||||
replies: Comment[]
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
comment: Comment
|
||||
showReplyForm: number | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'toggle-reply', commentId: number): void
|
||||
(event: 'add-reply', commentId: number, replyAuthor: string, replyContent: string): void
|
||||
}>()
|
||||
|
||||
const replyAuthor = ref('')
|
||||
const replyContent = ref('')
|
||||
|
||||
const toggleReply = (commentId: number) => {
|
||||
emit('toggle-reply', commentId)
|
||||
}
|
||||
|
||||
const addReply = (commentId: number, author: string, content: string) => {
|
||||
emit('add-reply', commentId, author, content)
|
||||
replyAuthor.value = ''
|
||||
replyContent.value = ''
|
||||
}
|
||||
const handleSubmit = () => {
|
||||
emit('add-reply', props.comment.id, replyAuthor.value, replyContent.value)
|
||||
}
|
||||
|
||||
const formatDate = (timestamp: string) => {
|
||||
const date = new Date(timestamp)
|
||||
return date.toLocaleString()
|
||||
}
|
||||
|
||||
let lastColor: string | null = null
|
||||
|
||||
const randomColor = () => {
|
||||
let newColor: string
|
||||
|
||||
do {
|
||||
const index = Math.floor(Math.random() * AppConfig.systemMainColor.length)
|
||||
newColor = AppConfig.systemMainColor[index]
|
||||
} while (newColor === lastColor)
|
||||
|
||||
lastColor = newColor
|
||||
return newColor
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.comment-module {
|
||||
margin-top: 40px;
|
||||
|
||||
.comment-item,
|
||||
.reply-item {
|
||||
.comment-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.avatar {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin-right: 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 30px;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
background-color: var(--main-color);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.name {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
color: var(--art-gray-800);
|
||||
}
|
||||
|
||||
.comment-info,
|
||||
.reply-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 10px 0;
|
||||
margin-top: 10px;
|
||||
|
||||
.date {
|
||||
font-size: 12px;
|
||||
color: var(--art-gray-500);
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
margin-left: 20px;
|
||||
font-size: 12px;
|
||||
color: var(--art-gray-800);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
color: var(--main-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.comment-replies {
|
||||
padding-left: 10px;
|
||||
|
||||
.reply-item {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user