chore: opt template style
This commit is contained in:
@@ -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
|
||||
|
||||
Generated
+16
-16
@@ -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
@@ -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)
|
||||
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,17 +289,12 @@ watch(() => props.open, (newVal) => {
|
||||
</DialogHeader>
|
||||
<div class="space-y-4 py-4">
|
||||
<!-- 基本信息 -->
|
||||
<div class="space-y-2">
|
||||
<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="space-y-2">
|
||||
<Label for="description">描述</Label>
|
||||
<Textarea id="description" v-model="formData.description" placeholder="请输入模板描述" />
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="col-span-3 space-y-2">
|
||||
<Label>状态</Label>
|
||||
<Select v-model="formData.status">
|
||||
<SelectTrigger class="w-full">
|
||||
@@ -302,6 +308,12 @@ watch(() => props.open, (newVal) => {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="description">描述</Label>
|
||||
<Textarea id="description" v-model="formData.description" placeholder="请输入模板描述" />
|
||||
</div>
|
||||
|
||||
<!-- 占位符配置 -->
|
||||
<div class="space-y-2">
|
||||
@@ -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,14 +269,18 @@ 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">
|
||||
<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">
|
||||
@@ -299,6 +303,9 @@ watch(() => props.open, (newVal) => {
|
||||
</ComboboxList>
|
||||
</Combobox>
|
||||
</div>
|
||||
<Button size="sm" variant="outline" @click="handleAddSubmit">添加实例</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 渠道配置表单 -->
|
||||
<div v-if="currentChannelConfig" class="mt-4">
|
||||
@@ -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>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as Checkbox } from "./Checkbox.vue"
|
||||
Reference in New Issue
Block a user