chore: opt template style

This commit is contained in:
engigu
2025-12-06 22:38:08 +08:00
parent fb35769e52
commit 5d3bfaa2de
9 changed files with 142 additions and 94 deletions
+5 -1
View File
@@ -45,7 +45,11 @@ func (t *Template) Add() error {
// Update 更新消息模板
func (t *Template) Update() error {
if err := db.Model(&Template{}).Where("id = ?", t.ID).Updates(t).Error; err != nil {
// 使用 Select 明确指定要更新的字段,包括布尔值字段,排除不应更新的时间戳字段
if err := db.Model(&Template{}).Where("id = ?", t.ID).
Select("name", "description", "text_template", "html_template", "markdown_template",
"placeholders", "at_mobiles", "at_user_ids", "is_at_all", "status", "modified_by").
Updates(t).Error; err != nil {
return err
}
return nil
+16 -16
View File
@@ -10,13 +10,13 @@
"dependencies": {
"@tailwindcss/vite": "^4.1.11",
"@tanstack/vue-table": "^8.21.3",
"@vueuse/core": "^13.6.0",
"@vueuse/core": "^13.9.0",
"axios": "^1.12.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-vue-next": "^0.536.0",
"pinia": "^3.0.3",
"reka-ui": "^2.4.1",
"reka-ui": "^2.6.1",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.11",
"tw-animate-css": "^1.3.6",
@@ -1605,14 +1605,14 @@
}
},
"node_modules/@vueuse/core": {
"version": "13.6.0",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-13.6.0.tgz",
"integrity": "sha512-DJbD5fV86muVmBgS9QQPddVX7d9hWYswzlf4bIyUD2dj8GC46R1uNClZhVAmsdVts4xb2jwp1PbpuiA50Qee1A==",
"version": "13.9.0",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-13.9.0.tgz",
"integrity": "sha512-ts3regBQyURfCE2BcytLqzm8+MmLlo5Ln/KLoxDVcsZ2gzIwVNnQpQOL/UKV8alUqjSZOlpFZcRNsLRqj+OzyA==",
"license": "MIT",
"dependencies": {
"@types/web-bluetooth": "^0.0.21",
"@vueuse/metadata": "13.6.0",
"@vueuse/shared": "13.6.0"
"@vueuse/metadata": "13.9.0",
"@vueuse/shared": "13.9.0"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
@@ -1622,9 +1622,9 @@
}
},
"node_modules/@vueuse/core/node_modules/@vueuse/shared": {
"version": "13.6.0",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-13.6.0.tgz",
"integrity": "sha512-pDykCSoS2T3fsQrYqf9SyF0QXWHmcGPQ+qiOVjlYSzlWd9dgppB2bFSM1GgKKkt7uzn0BBMV3IbJsUfHG2+BCg==",
"version": "13.9.0",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-13.9.0.tgz",
"integrity": "sha512-e89uuTLMh0U5cZ9iDpEI2senqPGfbPRTHM/0AaQkcxnpqjkZqDYP8rpfm7edOz8s+pOCOROEy1PIveSW8+fL5g==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
@@ -1634,9 +1634,9 @@
}
},
"node_modules/@vueuse/metadata": {
"version": "13.6.0",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-13.6.0.tgz",
"integrity": "sha512-rnIH7JvU7NjrpexTsl2Iwv0V0yAx9cw7+clymjKuLSXG0QMcLD0LDgdNmXic+qL0SGvgSVPEpM9IDO/wqo1vkQ==",
"version": "13.9.0",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-13.9.0.tgz",
"integrity": "sha512-1AFRvuiGphfF7yWixZa0KwjYH8ulyjDCC0aFgrGRz8+P4kvDFSdXLVfTk5xAN9wEuD1J6z4/myMoYbnHoX07zg==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
@@ -2632,9 +2632,9 @@
"license": "MIT"
},
"node_modules/reka-ui": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.4.1.tgz",
"integrity": "sha512-NB7DrCsODN8MH02BWtgiExygfFcuuZ5/PTn6fMgjppmFHqePvNhmSn1LEuF35nel6PFbA4v+gdj0IoGN1yZ+vw==",
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.6.1.tgz",
"integrity": "sha512-XK7cJDQoNuGXfCNzBBo/81Yg/OgjPwvbabnlzXG2VsdSgNsT6iIkuPBPr+C0Shs+3bb0x0lbPvgQAhMSCKm5Ww==",
"license": "MIT",
"dependencies": {
"@floating-ui/dom": "^1.6.13",
+2 -2
View File
@@ -13,13 +13,13 @@
"dependencies": {
"@tailwindcss/vite": "^4.1.11",
"@tanstack/vue-table": "^8.21.3",
"@vueuse/core": "^13.6.0",
"@vueuse/core": "^13.9.0",
"axios": "^1.12.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-vue-next": "^0.536.0",
"pinia": "^3.0.3",
"reka-ui": "^2.4.1",
"reka-ui": "^2.6.1",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.11",
"tw-animate-css": "^1.3.6",
@@ -7,6 +7,7 @@ import { Badge } from '@/components/ui/badge'
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import EmptyTableState from '@/components/ui/EmptyTableState.vue'
import Pagination from '@/components/ui/Pagination.vue'
import ClickableTruncate from '@/components/ui/ClickableTruncate.vue'
import TemplateApiViewer from './TemplateApiViewer.vue'
import TemplateInstanceConfig from './TemplateInstanceConfig.vue'
import TemplateEditor from './TemplateEditor.vue'
@@ -201,9 +202,11 @@ onMounted(async () => {
<!-- 数据行 -->
<TableRow v-for="item in state.tableData" :key="item.id">
<TableCell>{{ item.id }}</TableCell>
<TableCell class="font-medium">{{ item.name }}</TableCell>
<TableCell>
<div class="max-w-xs truncate">{{ item.description || '-' }}</div>
<ClickableTruncate :text="item.name" wrapper-class="max-w-[80px] sm:max-w-[100px]" preview-title="模板名称" />
</TableCell>
<TableCell>
<ClickableTruncate :text="item.description || '-'" wrapper-class="max-w-[80px] sm:max-w-[130px]" preview-title="模板描述" />
</TableCell>
<TableCell>
<div class="flex gap-1">
@@ -7,6 +7,7 @@ import { Textarea } from '@/components/ui/textarea'
import { Label } from '@/components/ui/label'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Checkbox } from '@/components/ui/checkbox'
import { toast } from 'vue-sonner'
import { request } from '@/api/api'
@@ -219,7 +220,7 @@ const loadTemplateData = (template: TemplateData) => {
placeholders: template.placeholders,
at_mobiles: template.at_mobiles || '',
at_user_ids: template.at_user_ids || '',
is_at_all: template.is_at_all || false,
is_at_all: Boolean(template.is_at_all),
status: template.status
}
@@ -244,17 +245,27 @@ const saveTemplate = async () => {
return
}
// 验证至少填写一种格式的模板内容
if (!formData.value.text_template && !formData.value.html_template && !formData.value.markdown_template) {
toast.error('至少需要填写一种格式的模板内容')
return
}
// 同步占位符数据
formData.value.placeholders = JSON.stringify(placeholdersList.value)
try {
const url = props.isEditing ? '/templates/edit' : '/templates/add'
await request.post(url, formData.value)
toast.success(props.isEditing ? '更新模板成功' : '添加模板成功')
emit('update:open', false)
emit('saved')
const response = await request.post(url, formData.value)
if (response.data.code === 200) {
toast.success(props.isEditing ? '更新模板成功' : '添加模板成功')
emit('update:open', false)
emit('saved')
} else {
toast.error(response.data.msg || '操作失败')
}
} catch (error: any) {
toast.error(error.response?.data?.message || '操作失败')
toast.error(error.response?.data?.msg || error.response?.data?.message || '操作失败')
}
}
@@ -278,9 +289,25 @@ watch(() => props.open, (newVal) => {
</DialogHeader>
<div class="space-y-4 py-4">
<!-- 基本信息 -->
<div class="space-y-2">
<Label for="name">模板名称 *</Label>
<Input id="name" v-model="formData.name" placeholder="请输入模板名称" />
<div class="grid grid-cols-10 gap-4">
<div class="col-span-7 space-y-2">
<Label for="name">模板名称 *</Label>
<Input id="name" v-model="formData.name" placeholder="请输入模板名称" />
</div>
<div class="col-span-3 space-y-2">
<Label>状态</Label>
<Select v-model="formData.status">
<SelectTrigger class="w-full">
<SelectValue placeholder="全部" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="enabled">启用</SelectItem>
<SelectItem value="disabled">禁用</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
</div>
<div class="space-y-2">
@@ -288,21 +315,6 @@ watch(() => props.open, (newVal) => {
<Textarea id="description" v-model="formData.description" placeholder="请输入模板描述" />
</div>
<div class="space-y-2">
<Label>状态</Label>
<Select v-model="formData.status">
<SelectTrigger class="w-full">
<SelectValue placeholder="全部" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="enabled">启用</SelectItem>
<SelectItem value="disabled">禁用</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
<!-- 占位符配置 -->
<div class="space-y-2">
<div class="flex justify-between items-center">
@@ -337,11 +349,10 @@ watch(() => props.open, (newVal) => {
<Label>@提醒配置 <span class="text-xs text-muted-foreground font-normal">适用于钉钉企业微信等</span></Label>
<div class="grid grid-cols-1 md:grid-cols-3 gap-2">
<div class="flex items-center gap-2 px-3 py-2 border rounded-md">
<input
type="checkbox"
<Checkbox
id="is_at_all"
v-model="formData.is_at_all"
class="w-4 h-4 rounded border-gray-300"
:model-value="formData.is_at_all"
@update:model-value="(newVal: boolean | 'indeterminate') => formData.is_at_all = newVal === true"
/>
<Label for="is_at_all" class="cursor-pointer text-sm">@所有人</Label>
</div>
@@ -269,35 +269,42 @@ watch(() => props.open, (newVal) => {
<div class="space-y-4">
<!-- 模板信息 -->
<div class="mb-6 p-4 bg-muted rounded-lg">
<div class="text-sm text-muted-foreground">模板名称</div>
<div class="text-lg font-medium">{{ templateData?.name }}</div>
<div class="text-xs text-muted-foreground mt-1">ID: {{ templateData?.id }}</div>
<div class="mb-6 p-3 bg-muted rounded-lg space-y-1">
<div class="flex items-baseline gap-2">
<span class="text-base font-semibold">{{ templateData?.name }}</span>
<Badge variant="outline" class="text-xs">{{ templateData?.id }}</Badge>
</div>
<div class="text-xs text-muted-foreground">为此模板配置发送实例</div>
</div>
<!-- 添加实例表单 -->
<div class="space-y-4">
<Label class="text-sm font-medium">选择发送渠道</Label>
<Combobox v-model="channelName" @update:model-value="handlechannelNameChange">
<ComboboxAnchor class="w-full">
<ComboboxInput v-model="inputDisplayValue" @input="handleSearch(inputDisplayValue)"
class="flex h-10 w-full" placeholder="搜索或选择渠道类型进行实例的添加..." />
</ComboboxAnchor>
<ComboboxList class="w-[var(--reka-combobox-trigger-width)]">
<ComboboxViewport>
<ComboboxItem v-for="option in displayOptions" :key="option.id" :value="option.name">
<div class="flex items-center justify-between w-full">
<span>{{ option.name }}</span>
<CheckIcon v-if="channelName === option.name" class="h-4 w-4" />
</div>
</ComboboxItem>
<div v-if="isSearching" class="p-2 text-sm text-muted-foreground">搜索中...</div>
<div v-if="!isSearching && displayOptions.length === 0 && searchQuery" class="p-2 text-sm text-muted-foreground">
未找到匹配的渠道
</div>
</ComboboxViewport>
</ComboboxList>
</Combobox>
<div class="flex items-end gap-2">
<div class="flex-1 space-y-2">
<Label class="text-sm font-medium">选择发送渠道</Label>
<Combobox v-model="channelName" @update:model-value="handlechannelNameChange">
<ComboboxAnchor class="w-full">
<ComboboxInput v-model="inputDisplayValue" @input="handleSearch(inputDisplayValue)"
class="flex h-10 w-full" placeholder="搜索或选择渠道类型进行实例的添加..." />
</ComboboxAnchor>
<ComboboxList class="w-[var(--reka-combobox-trigger-width)]">
<ComboboxViewport>
<ComboboxItem v-for="option in displayOptions" :key="option.id" :value="option.name">
<div class="flex items-center justify-between w-full">
<span>{{ option.name }}</span>
<CheckIcon v-if="channelName === option.name" class="h-4 w-4" />
</div>
</ComboboxItem>
<div v-if="isSearching" class="p-2 text-sm text-muted-foreground">搜索中...</div>
<div v-if="!isSearching && displayOptions.length === 0 && searchQuery" class="p-2 text-sm text-muted-foreground">
未找到匹配的渠道
</div>
</ComboboxViewport>
</ComboboxList>
</Combobox>
</div>
<Button size="sm" variant="outline" @click="handleAddSubmit">添加实例</Button>
</div>
</div>
<!-- 渠道配置表单 -->
@@ -326,10 +333,6 @@ watch(() => props.open, (newVal) => {
</div>
</div>
<div class="flex justify-end gap-2 border-b pb-4 mt-2">
<Button @click="handleAddSubmit">添加实例</Button>
</div>
<!-- 关联的实例表 -->
<div class="mt-4">
<h3 class="text-sm font-medium mb-3">已经关联的实例</h3>
@@ -82,11 +82,6 @@ const handlechannelNameChange = () => {
}
}
// 关闭drawer
const handleClose = () => {
emit('update:open', false)
}
// 添加单条实例配置
const handleAddSubmit = async () => {
// 组建表单数据
@@ -244,19 +239,19 @@ onMounted(() => {
<div v-if="props.editData" class="flex flex-col sm:flex-row sm:items-center gap-2 border-b p-4">
<Label class="w-16 sm:w-16">任务名称</Label>
<Input v-model="props.editData.name" placeholder="请输入任务名称" class="w-full sm:w-64" />
<Button class="w-full sm:w-auto sm:ml-auto" @click="handleEditTask">修改</Button>
<Button size="sm" variant="outline" class="w-full sm:w-auto sm:ml-auto" @click="handleEditTask">修改</Button>
</div>
<div class="mt-4">
<div class="flex gap-4">
<div class="flex-1">
<div class="flex items-end gap-2">
<div class="flex-1 space-y-2">
<Label class="text-sm font-medium">选择发送渠道</Label>
<Combobox v-model="channelName" @update:model-value="handlechannelNameChange">
<ComboboxAnchor class="w-full">
<ComboboxInput v-model="inputDisplayValue" @input="handleSearch(inputDisplayValue)"
class="flex h-10 w-full " placeholder="搜索或选择渠道类型进行实例的添加..." />
class="flex h-10 w-full" placeholder="搜索或选择渠道类型进行实例的添加..." />
</ComboboxAnchor>
<ComboboxList class="w-[var(--reka-combobox-trigger-width)]">
<ComboboxViewport>
@@ -276,6 +271,7 @@ onMounted(() => {
</ComboboxList>
</Combobox>
</div>
<Button size="sm" variant="outline" @click="handleAddSubmit">添加实例</Button>
</div>
<!-- 动态任务配置区域 -->
<div v-if="currentChannelConfig" class="mt-4">
@@ -309,11 +305,6 @@ onMounted(() => {
</div>
</div>
<div class="flex justify-end gap-2 border-b pb-4 mt-2">
<Button variant="outline" @click="handleClose">取消</Button>
<Button @click="handleAddSubmit">添加实例</Button>
</div>
<!-- 关联的实例表 -->
<div class="mt-4">
<h3 class="text-sm font-medium text-gray-900 mb-3">已经关联的实例</h3>
@@ -0,0 +1,35 @@
<script setup lang="ts">
import type { CheckboxRootEmits, CheckboxRootProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { Check } from "lucide-vue-next"
import { CheckboxIndicator, CheckboxRoot, useForwardPropsEmits } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<CheckboxRootProps & { class?: HTMLAttributes["class"] }>()
const emits = defineEmits<CheckboxRootEmits>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<CheckboxRoot
v-slot="slotProps"
data-slot="checkbox"
v-bind="forwarded"
:class="
cn('peer border-input data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
props.class)"
>
<CheckboxIndicator
data-slot="checkbox-indicator"
class="grid place-content-center text-current transition-none"
>
<slot v-bind="slotProps">
<Check class="size-3.5" />
</slot>
</CheckboxIndicator>
</CheckboxRoot>
</template>
+1
View File
@@ -0,0 +1 @@
export { default as Checkbox } from "./Checkbox.vue"