fix: 提交标签同步
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import request from '@/config/axios'
|
||||
import type { Dayjs } from 'dayjs';
|
||||
|
||||
/** 自定义标签类型:product-产品, store-店铺, supplier-供货商 */
|
||||
export type CustomTagType = 'product' | 'store' | 'supplier'
|
||||
/** 自定义标签类型:product-产品, store-店铺, supplier-供货商, member-会员 */
|
||||
export type CustomTagType = 'product' | 'store' | 'supplier' | 'member'
|
||||
|
||||
/** 自定义标签信息 */
|
||||
export interface CustomTag {
|
||||
|
||||
@@ -66,7 +66,7 @@ export const TagConfigApi = {
|
||||
deleteTagConfig: (id: number) =>
|
||||
request.delete({ url: '/ydoyun/tag-config/delete?id=' + id }),
|
||||
manualSync: (tagConfigId: number) =>
|
||||
request.post({ url: '/ydoyun/tag-config/manual-sync?tagConfigId=' + tagConfigId }),
|
||||
request.post({ url: '/ydoyun/tag-config/manual-sync?tagConfigId=' + tagConfigId })
|
||||
}
|
||||
|
||||
export const TagApi = {
|
||||
@@ -90,3 +90,22 @@ export const TagSyncHistoryApi = {
|
||||
getTagSyncHistory: (id: number) =>
|
||||
request.get({ url: '/ydoyun/tag-sync-history/get?id=' + id }),
|
||||
}
|
||||
|
||||
/** 标签同步详情 */
|
||||
export interface TagSyncDetail {
|
||||
id?: number
|
||||
syncHistoryId?: number
|
||||
tagId?: number
|
||||
tagName?: string
|
||||
tagType?: string
|
||||
sqlExecuted?: string
|
||||
execStatus?: string
|
||||
recordCount?: number
|
||||
errorMessage?: string
|
||||
execOrder?: number
|
||||
}
|
||||
|
||||
export const TagSyncDetailApi = {
|
||||
getDetailListBySyncHistoryId: (syncHistoryId: number) =>
|
||||
request.get({ url: '/ydoyun/tag-sync-detail/list-by-sync-history-id?syncHistoryId=' + syncHistoryId }),
|
||||
}
|
||||
|
||||
@@ -831,6 +831,16 @@ const remainingRouter: AppRouteRecordRaw[] = [
|
||||
hidden: true
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'custom-tag',
|
||||
name: 'CustomTag',
|
||||
meta: {
|
||||
title: '定制标签',
|
||||
noCache: true,
|
||||
canTo: true
|
||||
},
|
||||
component: () => import(/* webpackChunkName: "custom-tag" */ '@/views/ydoyun/customtag/index.vue')
|
||||
},
|
||||
{
|
||||
path: 'product-custom-tag',
|
||||
name: 'ProductCustomTag',
|
||||
|
||||
@@ -96,6 +96,10 @@ defineOptions({ name: 'Login' })
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.bg-container {
|
||||
@@ -199,10 +203,10 @@ defineOptions({ name: 'Login' })
|
||||
width: 100%;
|
||||
max-width: 1300px;
|
||||
margin: 0 auto;
|
||||
padding: 80px 40px 100px;
|
||||
padding: 40px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
align-items: center;
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Dialog :title="dialogTitle" v-model="dialogVisible" width="560px">
|
||||
<Dialog :title="dialogTitle" v-model="dialogVisible" width="720px">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
@@ -7,9 +7,18 @@
|
||||
label-width="110px"
|
||||
v-loading="formLoading"
|
||||
>
|
||||
<el-form-item label="标签类别" prop="type">
|
||||
<el-select v-model="formData.type" placeholder="请选择标签类别" class="!w-full" @change="loadInitialSqlByType">
|
||||
<el-option label="产品 (product)" value="product" />
|
||||
<el-option label="门店 (store)" value="store" />
|
||||
<el-option label="供货商 (supplier)" value="supplier" />
|
||||
<el-option label="会员 (member)" value="member" />
|
||||
</el-select>
|
||||
<div class="form-tip">选择与标准标签一致的类别,名称不能与同类型标准标签重复</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="标签名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入标签名称,如:销售区域" maxlength="50" show-word-limit />
|
||||
<div class="form-tip">用于报表中展示的标签名称</div>
|
||||
<el-input v-model="formData.name" placeholder="请输入标签名称,如:销售区域" maxlength="50" show-word-limit @blur="validateNameWithStandardTags" />
|
||||
<div class="form-tip">用于报表中展示的标签名称,不能与同类型标准标签重名</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="公式描述" prop="expression">
|
||||
<el-input
|
||||
@@ -45,13 +54,22 @@
|
||||
<div class="form-tip">用于报表展示时的颜色标识,可点击预设色或使用取色盘</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="SQL 脚本" prop="sqlScript">
|
||||
<el-input
|
||||
v-model="formData.sqlScript"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入 SQL 脚本,用于获取标签数据"
|
||||
/>
|
||||
<div class="form-tip">执行 SQL 获取标签选项,支持多列,第一列作为显示值</div>
|
||||
<div class="sql-script-field">
|
||||
<div class="param-ref-row">
|
||||
<el-button size="small" type="success" plain @click="insertInsertTemplate">
|
||||
插入 {{ insertTableName }} 模板
|
||||
</el-button>
|
||||
</div>
|
||||
<el-input
|
||||
ref="sqlScriptInputRef"
|
||||
v-model="formData.sqlScript"
|
||||
type="textarea"
|
||||
:rows="8"
|
||||
placeholder="请输入标签同步时执行的 SQL 脚本,支持多行。可点击上方按钮插入 INSERT 模板"
|
||||
class="sql-script-area"
|
||||
/>
|
||||
<div class="form-tip">该 SQL 将用于标签同步,将结果写入对应标签表。支持多列,第一列作为 code,第二列作为 name</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
@@ -62,6 +80,7 @@
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { CustomTagApi, CustomTag } from '@/api/ydoyun/customtag'
|
||||
import { TagApi } from '@/api/ydoyun/tagconfig'
|
||||
|
||||
defineOptions({ name: 'CustomTagForm' })
|
||||
|
||||
@@ -75,17 +94,77 @@ const formType = ref('')
|
||||
|
||||
const formData = ref({
|
||||
id: undefined,
|
||||
type: undefined,
|
||||
name: undefined,
|
||||
expression: undefined,
|
||||
color: undefined,
|
||||
sqlScript: undefined
|
||||
})
|
||||
|
||||
const validateNameWithStandardTags = async (_rule: any, value: string, callback: (e?: Error) => void) => {
|
||||
if (!value?.trim()) return callback()
|
||||
const type = formData.value.type
|
||||
if (!type) return callback()
|
||||
try {
|
||||
const list = await TagApi.getTagListByType(type)
|
||||
const res = list as any
|
||||
const tags = res?.data ?? res ?? []
|
||||
const exists = Array.isArray(tags) && tags.some((t: any) => (t.name || '').trim() === value.trim())
|
||||
if (exists) {
|
||||
callback(new Error('该名称与同类型标准标签重复,请更换'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
} catch {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const formRules = reactive({
|
||||
name: [{ required: true, message: '标签名称不能为空', trigger: 'blur' }]
|
||||
type: [{ required: true, message: '请选择标签类别', trigger: 'change' }],
|
||||
name: [
|
||||
{ required: true, message: '标签名称不能为空', trigger: 'blur' },
|
||||
{ validator: validateNameWithStandardTags, trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
const formRef = ref()
|
||||
const sqlScriptInputRef = ref()
|
||||
|
||||
const INSERT_TABLE_MAP: Record<string, string> = {
|
||||
product: 'ydoyun_tag_product',
|
||||
supplier: 'ydoyun_tag_supplier',
|
||||
store: 'ydoyun_tag_store',
|
||||
member: 'ydoyun_tag_member'
|
||||
}
|
||||
|
||||
const insertTableName = computed(() => {
|
||||
const type = formData.value.type || 'product'
|
||||
return INSERT_TABLE_MAP[type] || 'ydoyun_tag_product'
|
||||
})
|
||||
|
||||
const getInsertTemplate = () => {
|
||||
const table = insertTableName.value
|
||||
return `INSERT INTO ${table} (code, name, color)
|
||||
SELECT code, name, color FROM (
|
||||
-- 在此编写您的查询逻辑,将 code、name、color 替换为实际字段,color 用于保存标签颜色
|
||||
SELECT '' AS code, '' AS name, '' AS color FROM (SELECT 1) _ LIMIT 0
|
||||
) t
|
||||
`
|
||||
}
|
||||
|
||||
const insertInsertTemplate = () => {
|
||||
const template = getInsertTemplate()
|
||||
const oldScript = formData.value.sqlScript || ''
|
||||
formData.value.sqlScript = template + (oldScript ? '\n' + oldScript : '')
|
||||
}
|
||||
|
||||
/** 根据类型加载初始 SQL(仅新增且当前为空时) */
|
||||
const loadInitialSqlByType = () => {
|
||||
if (formType.value !== 'create' || formData.value.sqlScript?.trim()) return
|
||||
if (!formData.value.type) return
|
||||
formData.value.sqlScript = getInsertTemplate()
|
||||
}
|
||||
|
||||
/** 预设颜色选项 */
|
||||
const presetColors = [
|
||||
@@ -114,6 +193,8 @@ const open = async (type: string, id?: number) => {
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
} else {
|
||||
loadInitialSqlByType()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +205,8 @@ const submitForm = async () => {
|
||||
await formRef.value.validate()
|
||||
formLoading.value = true
|
||||
try {
|
||||
const data = formData.value as unknown as CustomTag
|
||||
const base = { ...formData.value }
|
||||
const data = (formType.value === 'create' ? { ...base, useParams: false } : base) as unknown as CustomTag
|
||||
if (formType.value === 'create') {
|
||||
await CustomTagApi.createCustomTag(data)
|
||||
message.success(t('common.createSuccess'))
|
||||
@@ -143,6 +225,7 @@ const submitForm = async () => {
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
id: undefined,
|
||||
type: undefined,
|
||||
name: undefined,
|
||||
expression: undefined,
|
||||
color: undefined,
|
||||
@@ -200,4 +283,18 @@ const resetForm = () => {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.sql-script-field { width: 100%; }
|
||||
.param-ref-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.sql-script-area { width: 100%; }
|
||||
.sql-script-area :deep(textarea) {
|
||||
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Dialog :title="dialogTitle" v-model="dialogVisible" width="560px">
|
||||
<Dialog :title="dialogTitle" v-model="dialogVisible" width="720px">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
@@ -7,9 +7,18 @@
|
||||
label-width="110px"
|
||||
v-loading="formLoading"
|
||||
>
|
||||
<el-form-item label="标签类型" prop="type">
|
||||
<el-select v-model="formData.type" placeholder="请选择标签类型" class="!w-full" :disabled="!!props.tagType">
|
||||
<el-option label="产品 (product)" value="product" />
|
||||
<el-option label="门店 (store)" value="store" />
|
||||
<el-option label="供货商 (supplier)" value="supplier" />
|
||||
<el-option label="会员 (member)" value="member" />
|
||||
</el-select>
|
||||
<div class="form-tip">选择与标准标签一致的类别,名称不能与同类型标准标签重复</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="标签名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入标签名称" maxlength="50" show-word-limit />
|
||||
<div class="form-tip">用于报表中展示的标签名称</div>
|
||||
<el-input v-model="formData.name" placeholder="请输入标签名称" maxlength="50" show-word-limit @blur="validateNameWithStandardTags" />
|
||||
<div class="form-tip">用于报表中展示的标签名称,不能与同类型标准标签重名</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="公式描述" prop="expression">
|
||||
<el-input
|
||||
@@ -52,30 +61,23 @@
|
||||
</el-form-item>
|
||||
<el-form-item label="SQL 脚本" prop="sqlScript">
|
||||
<div class="sql-script-field">
|
||||
<div v-if="formData.useParams === 1" class="param-insert-bar">
|
||||
<span class="param-label">插入参数变量:</span>
|
||||
<el-select
|
||||
v-model="selectedParamIndex"
|
||||
:placeholder="paramList.length > 0 ? '选择参数后点击插入' : '请先填写上方参数列表'"
|
||||
clearable
|
||||
class="param-select"
|
||||
:disabled="paramList.length === 0"
|
||||
>
|
||||
<el-option
|
||||
<div class="param-ref-row">
|
||||
<el-button size="small" type="success" plain @click="insertInsertTemplate">
|
||||
插入 {{ insertTableName }} 模板
|
||||
</el-button>
|
||||
<template v-if="formData.useParams === 1">
|
||||
<span class="param-ref-label">引用参数:</span>
|
||||
<el-button
|
||||
v-for="(p, idx) in paramList"
|
||||
:key="idx"
|
||||
:label="`参数${idx + 1}: ${p}`"
|
||||
:value="idx"
|
||||
/>
|
||||
</el-select>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
:disabled="paramList.length === 0 || selectedParamIndex === null"
|
||||
@click="selectedParamIndex != null ? insertParamVar(selectedParamIndex) : null"
|
||||
>
|
||||
插入
|
||||
</el-button>
|
||||
size="small"
|
||||
type="info"
|
||||
plain
|
||||
@click="insertParamRef(p, idx)"
|
||||
>
|
||||
引用{{ p }}
|
||||
</el-button>
|
||||
</template>
|
||||
</div>
|
||||
<el-input
|
||||
ref="sqlScriptInputRef"
|
||||
@@ -112,6 +114,7 @@
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { CustomTagApi, CustomTag, CustomTagType } from '@/api/ydoyun/customtag'
|
||||
import { TagApi } from '@/api/ydoyun/tagconfig'
|
||||
|
||||
defineOptions({ name: 'CustomTagFormWithParams' })
|
||||
|
||||
@@ -138,14 +141,85 @@ const formData = ref<Partial<CustomTag>>({
|
||||
params: undefined
|
||||
})
|
||||
|
||||
const validateNameWithStandardTags = async (_rule: any, value: string, callback: (e?: Error) => void) => {
|
||||
if (!value?.trim()) return callback()
|
||||
const type = formData.value.type ?? props.tagType
|
||||
if (!type) return callback()
|
||||
try {
|
||||
const list = await TagApi.getTagListByType(type)
|
||||
const res = list as any
|
||||
const tags = res?.data ?? res ?? []
|
||||
const exists = Array.isArray(tags) && tags.some((t: any) => (t.name || '').trim() === value.trim())
|
||||
if (exists) {
|
||||
callback(new Error('该名称与同类型标准标签重复,请更换'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
} catch {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const formRules = reactive({
|
||||
name: [{ required: true, message: '标签名称不能为空', trigger: 'blur' }]
|
||||
type: [{ required: true, message: '请选择标签类型', trigger: 'change' }],
|
||||
name: [
|
||||
{ required: true, message: '标签名称不能为空', trigger: 'blur' },
|
||||
{ validator: validateNameWithStandardTags, trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
const formRef = ref()
|
||||
const sqlScriptInputRef = ref()
|
||||
const sqlCursorPos = ref({ start: 0, end: 0 })
|
||||
const selectedParamIndex = ref<number | null>(null)
|
||||
|
||||
const INSERT_TABLE_MAP: Record<string, string> = {
|
||||
product: 'ydoyun_tag_product',
|
||||
supplier: 'ydoyun_tag_supplier',
|
||||
store: 'ydoyun_tag_store',
|
||||
member: 'ydoyun_tag_member'
|
||||
}
|
||||
|
||||
const insertTableName = computed(() => {
|
||||
const type = formData.value.type ?? props.tagType ?? 'product'
|
||||
return INSERT_TABLE_MAP[type] || 'ydoyun_tag_product'
|
||||
})
|
||||
|
||||
const getInsertTemplate = () => {
|
||||
const table = insertTableName.value
|
||||
return `INSERT INTO ${table} (code, name, color)
|
||||
SELECT code, name, color FROM (
|
||||
-- 在此编写您的查询逻辑,将 code、name、color 替换为实际字段,color 用于保存标签颜色
|
||||
SELECT '' AS code, '' AS name, '' AS color FROM (SELECT 1) _ LIMIT 0
|
||||
) t
|
||||
`
|
||||
}
|
||||
|
||||
const insertInsertTemplate = () => {
|
||||
const template = getInsertTemplate()
|
||||
const oldScript = formData.value.sqlScript || ''
|
||||
formData.value.sqlScript = template + (oldScript ? '\n' + oldScript : '')
|
||||
}
|
||||
|
||||
const insertAtCursor = (text: string) => {
|
||||
const textarea = sqlScriptInputRef.value?.$el?.querySelector('textarea')
|
||||
if (textarea) {
|
||||
const start = textarea.selectionStart
|
||||
const end = textarea.selectionEnd
|
||||
const sql = formData.value.sqlScript || ''
|
||||
formData.value.sqlScript = sql.slice(0, start) + text + sql.slice(end)
|
||||
nextTick(() => {
|
||||
textarea.focus()
|
||||
const newPos = start + text.length
|
||||
textarea.setSelectionRange(newPos, newPos)
|
||||
})
|
||||
} else {
|
||||
formData.value.sqlScript = (formData.value.sqlScript || '') + text
|
||||
}
|
||||
}
|
||||
|
||||
const insertParamRef = (_p: string, idx: number) => {
|
||||
insertAtCursor(`\${参数${idx + 1}}`)
|
||||
}
|
||||
|
||||
const paramList = computed(() => {
|
||||
const p = formData.value.params
|
||||
@@ -160,24 +234,6 @@ const saveSqlCursor = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const insertParamVar = (index: number) => {
|
||||
const variable = `\${参数${index + 1}}`
|
||||
const current = formData.value.sqlScript || ''
|
||||
const { start, end } = sqlCursorPos.value
|
||||
const before = current.slice(0, start)
|
||||
const after = current.slice(end)
|
||||
formData.value.sqlScript = before + variable + after
|
||||
selectedParamIndex.value = null
|
||||
nextTick(() => {
|
||||
sqlCursorPos.value = { start: start + variable.length, end: start + variable.length }
|
||||
const textarea = sqlScriptInputRef.value?.$el?.querySelector('textarea')
|
||||
if (textarea) {
|
||||
textarea.focus()
|
||||
textarea.setSelectionRange(start + variable.length, start + variable.length)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const sqlSegments = computed(() => {
|
||||
const sql = formData.value.sqlScript || ''
|
||||
const regex = /\$\{([^}]+)\}/g
|
||||
@@ -241,9 +297,19 @@ const open = async (type: string, id?: number) => {
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
} else if (type === 'create') {
|
||||
loadInitialSqlByType()
|
||||
}
|
||||
}
|
||||
|
||||
/** 根据类型加载初始 SQL(仅新增且当前为空时) */
|
||||
const loadInitialSqlByType = () => {
|
||||
if (formType.value !== 'create' || formData.value.sqlScript?.trim()) return
|
||||
const type = formData.value.type ?? props.tagType
|
||||
if (!type) return
|
||||
formData.value.sqlScript = getInsertTemplate()
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
|
||||
const emit = defineEmits(['success'])
|
||||
@@ -294,12 +360,10 @@ const resetForm = () => {
|
||||
.color-picker-wrap { display: flex; align-items: center; gap: 12px; }
|
||||
.color-picker-wrap .color-input { flex: 1; min-width: 120px; }
|
||||
.sql-script-field { width: 100%; }
|
||||
.param-insert-bar {
|
||||
display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-bottom: 8px;
|
||||
padding: 8px 12px; background: var(--el-fill-color-light); border-radius: 6px;
|
||||
.param-ref-row {
|
||||
display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-bottom: 8px;
|
||||
}
|
||||
.param-label { font-size: 13px; color: var(--el-text-color-secondary); margin-right: 4px; }
|
||||
.param-select { width: 180px; }
|
||||
.param-ref-label { font-size: 13px; color: var(--el-text-color-regular); margin-right: 4px; }
|
||||
.sql-preview {
|
||||
margin-top: 10px; padding: 10px 12px; background: var(--el-fill-color-lighter);
|
||||
border-radius: 6px; font-family: monospace; font-size: 13px;
|
||||
|
||||
@@ -8,6 +8,14 @@
|
||||
:inline="true"
|
||||
label-width="90px"
|
||||
>
|
||||
<el-form-item label="标签类型" prop="type">
|
||||
<el-select v-model="queryParams.type" placeholder="全部" clearable class="!w-180px">
|
||||
<el-option label="产品" value="product" />
|
||||
<el-option label="门店" value="store" />
|
||||
<el-option label="供货商" value="supplier" />
|
||||
<el-option label="会员" value="member" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="标签名称" prop="name">
|
||||
<el-input
|
||||
v-model="queryParams.name"
|
||||
@@ -81,16 +89,20 @@
|
||||
@selection-change="handleRowCheckboxChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column label="标签名称" align="center" prop="name" min-width="120" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="公式描述" align="center" prop="expression" min-width="150" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="标签颜色" align="center" prop="color" width="100">
|
||||
<el-table-column label="标签名称" align="center" prop="name" min-width="120" :show-overflow-tooltip="true">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.color" :color="scope.row.color" effect="dark" size="small">
|
||||
{{ scope.row.color }}
|
||||
<el-tag v-if="scope.row.color" :color="scope.row.color" effect="dark" size="small" class="!border-0">
|
||||
{{ scope.row.name || '-' }}
|
||||
</el-tag>
|
||||
<span v-else>-</span>
|
||||
<span v-else>{{ scope.row.name || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="标签类型" align="center" prop="type" width="100">
|
||||
<template #default="scope">
|
||||
{{ typeLabelMap[scope.row.type] || scope.row.type || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="公式描述" align="center" prop="expression" min-width="150" :show-overflow-tooltip="true" />
|
||||
<el-table-column
|
||||
label="创建时间"
|
||||
align="center"
|
||||
@@ -146,9 +158,17 @@ const loading = ref(true)
|
||||
const list = ref<CustomTag[]>([])
|
||||
const total = ref(0)
|
||||
|
||||
const typeLabelMap: Record<string, string> = {
|
||||
product: '产品',
|
||||
store: '门店',
|
||||
supplier: '供货商',
|
||||
member: '会员'
|
||||
}
|
||||
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
type: undefined as string | undefined,
|
||||
name: undefined,
|
||||
expression: undefined,
|
||||
createTime: [] as string[]
|
||||
@@ -178,6 +198,7 @@ const handleQuery = () => {
|
||||
/** 重置 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields()
|
||||
queryParams.type = undefined
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,11 @@
|
||||
:inline="true"
|
||||
label-width="90px"
|
||||
>
|
||||
<el-form-item label="标签类型" prop="type">
|
||||
<el-select v-model="queryParams.type" placeholder="标签类型" class="!w-140px" disabled>
|
||||
<el-option label="产品" value="product" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="标签名称" prop="name">
|
||||
<el-input
|
||||
v-model="queryParams.name"
|
||||
@@ -62,14 +67,20 @@
|
||||
@selection-change="handleRowCheckboxChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column label="标签名称" align="center" prop="name" min-width="120" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="公式描述" align="center" prop="expression" min-width="150" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="标签颜色" align="center" prop="color" width="100">
|
||||
<el-table-column label="标签名称" align="center" prop="name" min-width="120" :show-overflow-tooltip="true">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.color" :color="scope.row.color" effect="dark" size="small">{{ scope.row.color }}</el-tag>
|
||||
<span v-else>-</span>
|
||||
<el-tag v-if="scope.row.color" :color="scope.row.color" effect="dark" size="small" class="!border-0">
|
||||
{{ scope.row.name || '-' }}
|
||||
</el-tag>
|
||||
<span v-else>{{ scope.row.name || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="标签类型" align="center" prop="type" width="90">
|
||||
<template #default="scope">
|
||||
{{ typeLabelMap[scope.row.type] || scope.row.type || '产品' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="公式描述" align="center" prop="expression" min-width="150" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="代入参数" align="center" prop="useParams" width="90">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.useParams === true || scope.row.useParams === 1" type="success" size="small">是</el-tag>
|
||||
@@ -107,6 +118,13 @@ defineOptions({ name: 'ProductCustomTag' })
|
||||
const message = useMessage()
|
||||
const { t } = useI18n()
|
||||
|
||||
const typeLabelMap: Record<string, string> = {
|
||||
product: '产品',
|
||||
store: '门店',
|
||||
supplier: '供货商',
|
||||
member: '会员'
|
||||
}
|
||||
|
||||
const loading = ref(true)
|
||||
const list = ref<CustomTag[]>([])
|
||||
const total = ref(0)
|
||||
|
||||
@@ -7,6 +7,11 @@
|
||||
:inline="true"
|
||||
label-width="90px"
|
||||
>
|
||||
<el-form-item label="标签类型" prop="type">
|
||||
<el-select v-model="queryParams.type" placeholder="标签类型" class="!w-140px" disabled>
|
||||
<el-option label="门店" value="store" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="标签名称" prop="name">
|
||||
<el-input
|
||||
v-model="queryParams.name"
|
||||
@@ -62,14 +67,20 @@
|
||||
@selection-change="handleRowCheckboxChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column label="标签名称" align="center" prop="name" min-width="120" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="公式描述" align="center" prop="expression" min-width="150" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="标签颜色" align="center" prop="color" width="100">
|
||||
<el-table-column label="标签名称" align="center" prop="name" min-width="120" :show-overflow-tooltip="true">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.color" :color="scope.row.color" effect="dark" size="small">{{ scope.row.color }}</el-tag>
|
||||
<span v-else>-</span>
|
||||
<el-tag v-if="scope.row.color" :color="scope.row.color" effect="dark" size="small" class="!border-0">
|
||||
{{ scope.row.name || '-' }}
|
||||
</el-tag>
|
||||
<span v-else>{{ scope.row.name || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="标签类型" align="center" prop="type" width="90">
|
||||
<template #default="scope">
|
||||
{{ typeLabelMap[scope.row.type] || scope.row.type || '门店' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="公式描述" align="center" prop="expression" min-width="150" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="代入参数" align="center" prop="useParams" width="90">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.useParams === true || scope.row.useParams === 1" type="success" size="small">是</el-tag>
|
||||
@@ -107,6 +118,13 @@ defineOptions({ name: 'StoreCustomTag' })
|
||||
const message = useMessage()
|
||||
const { t } = useI18n()
|
||||
|
||||
const typeLabelMap: Record<string, string> = {
|
||||
product: '产品',
|
||||
store: '门店',
|
||||
supplier: '供货商',
|
||||
member: '会员'
|
||||
}
|
||||
|
||||
const loading = ref(true)
|
||||
const list = ref<CustomTag[]>([])
|
||||
const total = ref(0)
|
||||
|
||||
@@ -7,6 +7,11 @@
|
||||
:inline="true"
|
||||
label-width="90px"
|
||||
>
|
||||
<el-form-item label="标签类型" prop="type">
|
||||
<el-select v-model="queryParams.type" placeholder="标签类型" class="!w-140px" disabled>
|
||||
<el-option label="供货商" value="supplier" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="标签名称" prop="name">
|
||||
<el-input
|
||||
v-model="queryParams.name"
|
||||
@@ -62,14 +67,20 @@
|
||||
@selection-change="handleRowCheckboxChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column label="标签名称" align="center" prop="name" min-width="120" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="公式描述" align="center" prop="expression" min-width="150" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="标签颜色" align="center" prop="color" width="100">
|
||||
<el-table-column label="标签名称" align="center" prop="name" min-width="120" :show-overflow-tooltip="true">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.color" :color="scope.row.color" effect="dark" size="small">{{ scope.row.color }}</el-tag>
|
||||
<span v-else>-</span>
|
||||
<el-tag v-if="scope.row.color" :color="scope.row.color" effect="dark" size="small" class="!border-0">
|
||||
{{ scope.row.name || '-' }}
|
||||
</el-tag>
|
||||
<span v-else>{{ scope.row.name || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="标签类型" align="center" prop="type" width="90">
|
||||
<template #default="scope">
|
||||
{{ typeLabelMap[scope.row.type] || scope.row.type || '供货商' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="公式描述" align="center" prop="expression" min-width="150" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="代入参数" align="center" prop="useParams" width="90">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.useParams === true || scope.row.useParams === 1" type="success" size="small">是</el-tag>
|
||||
@@ -107,6 +118,13 @@ defineOptions({ name: 'SupplierCustomTag' })
|
||||
const message = useMessage()
|
||||
const { t } = useI18n()
|
||||
|
||||
const typeLabelMap: Record<string, string> = {
|
||||
product: '产品',
|
||||
store: '门店',
|
||||
supplier: '供货商',
|
||||
member: '会员'
|
||||
}
|
||||
|
||||
const loading = ref(true)
|
||||
const list = ref<CustomTag[]>([])
|
||||
const total = ref(0)
|
||||
|
||||
@@ -1079,7 +1079,8 @@
|
||||
|
||||
<EditSqlModal v-model="editSqlModalVisible" :tag="editSqlTag" @confirm="onEditSqlConfirm" />
|
||||
|
||||
<el-drawer v-model="syncHistoryVisible" title="同步历史" size="600px" destroy-on-close>
|
||||
<Teleport to="body">
|
||||
<el-drawer v-model="syncHistoryVisible" title="同步历史" size="600px" direction="rtl" destroy-on-close append-to-body :z-index="3000">
|
||||
<el-form :model="historyQuery" :inline="true" class="mb-4">
|
||||
<el-form-item label="同步时间">
|
||||
<el-date-picker
|
||||
@@ -1093,15 +1094,16 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="同步类型">
|
||||
<el-select v-model="historyQuery.syncType" placeholder="全部" clearable style="width: 120px">
|
||||
<el-select v-model="historyQuery.syncType" placeholder="全部" clearable style="width: 120px" :teleported="false">
|
||||
<el-option label="手动" value="manual" />
|
||||
<el-option label="定时" value="cron" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="同步状态">
|
||||
<el-select v-model="historyQuery.syncStatus" placeholder="全部" clearable style="width: 120px">
|
||||
<el-select v-model="historyQuery.syncStatus" placeholder="全部" clearable style="width: 120px" :teleported="false">
|
||||
<el-option label="成功" value="success" />
|
||||
<el-option label="失败" value="fail" />
|
||||
<el-option label="部分成功" value="partial" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
@@ -1109,15 +1111,39 @@
|
||||
<el-button @click="resetHistoryQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table :data="syncHistoryList" v-loading="historyLoading" stripe>
|
||||
<el-table :data="syncHistoryList" v-loading="historyLoading" stripe row-key="id" @expand-change="onHistoryExpand">
|
||||
<el-table-column type="expand">
|
||||
<template #default="{ row }">
|
||||
<div class="sync-detail-expand">
|
||||
<div v-if="detailLoadingMap[row.id]" class="detail-loading">加载中...</div>
|
||||
<el-table v-else :data="detailMap[row.id] || []" size="small" border>
|
||||
<el-table-column label="标签名称" prop="tagName" width="120" />
|
||||
<el-table-column label="标签类型" prop="tagType" width="90" />
|
||||
<el-table-column label="状态" prop="execStatus" width="80">
|
||||
<template #default="{ row: d }">
|
||||
<el-tag :type="d.execStatus === 'success' ? 'success' : 'danger'" size="small">
|
||||
{{ d.execStatus === 'success' ? '成功' : '失败' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="执行SQL" prop="sqlExecuted" min-width="300" show-overflow-tooltip>
|
||||
<template #default="{ row: d }">
|
||||
<pre class="sql-executed clickable" @click="openSqlViewer(d)">{{ d.sqlExecuted || '-' }}</pre>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="错误信息" prop="errorMessage" width="180" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="同步时间" prop="syncTime" width="170" :formatter="dateFormatter" />
|
||||
<el-table-column label="同步类型" prop="syncType" width="80">
|
||||
<template #default="{ row }">{{ row.syncType === 'manual' ? '手动' : '定时' }}</template>
|
||||
<template #default="{ row }">{{ syncTypeOptions.find(o => o.value === row.syncType)?.label || row.syncType || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" prop="syncStatus" width="80">
|
||||
<el-table-column label="状态" prop="syncStatus" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.syncStatus === 'success' ? 'success' : 'danger'" size="small">
|
||||
{{ row.syncStatus === 'success' ? '成功' : '失败' }}
|
||||
<el-tag :type="row.syncStatus === 'success' ? 'success' : row.syncStatus === 'partial' ? 'warning' : 'danger'" size="small">
|
||||
{{ syncStatusOptions.find(o => o.value === row.syncStatus)?.label || row.syncStatus || '-' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -1132,14 +1158,41 @@
|
||||
class="mt-4"
|
||||
/>
|
||||
</el-drawer>
|
||||
|
||||
<!-- 执行SQL 查看弹窗 -->
|
||||
<el-dialog
|
||||
v-model="sqlViewerVisible"
|
||||
:title="sqlViewerTitle"
|
||||
width="700px"
|
||||
destroy-on-close
|
||||
:z-index="3100"
|
||||
class="sql-viewer-dialog"
|
||||
>
|
||||
<el-input
|
||||
v-model="sqlViewerContent"
|
||||
type="textarea"
|
||||
:rows="16"
|
||||
readonly
|
||||
class="sql-viewer-textarea"
|
||||
/>
|
||||
<template #footer>
|
||||
<span v-if="copySuccessTip" class="sql-copy-tip">已复制</span>
|
||||
<el-button type="primary" @click="copySqlContent">
|
||||
<Icon icon="ep:document-copy" class="mr-1" />
|
||||
复制
|
||||
</el-button>
|
||||
<el-button @click="sqlViewerVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessageBox, ElLoading } from 'element-plus'
|
||||
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus'
|
||||
import { dateFormatter, getDateRange } from '@/utils/formatTime'
|
||||
import dayjs from 'dayjs'
|
||||
import { TagConfigApi, TagApi, TagSyncHistoryApi } from '@/api/ydoyun/tagconfig'
|
||||
import { TagConfigApi, TagApi, TagSyncHistoryApi, TagSyncDetailApi } from '@/api/ydoyun/tagconfig'
|
||||
import type { Tag, TagConfig, TagSyncHistory } from '@/api/ydoyun/tagconfig'
|
||||
import { ReportDatabaseApi, type ReportDatabase } from '@/api/ydoyun/reportdatabase'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
@@ -1569,7 +1622,7 @@ const handleManualSync = async () => {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await message.confirm('确定要立即执行一次标签同步吗?', '手动同步')
|
||||
await message.confirm('确定要立即执行一次标签同步吗?将先初始化标签库表,再执行标签同步。', '手动同步')
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
@@ -1752,9 +1805,25 @@ const editPresetTag = (type: string, name: string) => {
|
||||
openEditSqlModal(tag)
|
||||
}
|
||||
|
||||
const formatTime = (v: string) => {
|
||||
if (!v) return '-'
|
||||
return v.replace('T', ' ').slice(0, 19)
|
||||
const formatTime = (v: string | Date | number | null | undefined) => {
|
||||
if (v == null || v === '') return '-'
|
||||
let date: Date
|
||||
if (typeof v === 'number') {
|
||||
date = v > 1e12 ? new Date(v) : new Date(v * 1000)
|
||||
} else if (v instanceof Date) {
|
||||
date = v
|
||||
} else if (typeof v === 'string') {
|
||||
const n = Number(v)
|
||||
if (!isNaN(n)) {
|
||||
date = n > 1e12 ? new Date(n) : new Date(n * 1000)
|
||||
} else {
|
||||
date = new Date(v)
|
||||
}
|
||||
} else {
|
||||
return '-'
|
||||
}
|
||||
if (isNaN(date.getTime())) return '-'
|
||||
return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
|
||||
const syncHistoryVisible = ref(false)
|
||||
@@ -1763,6 +1832,16 @@ const syncHistoryList = ref<TagSyncHistory[]>([])
|
||||
const historyTotal = ref(0)
|
||||
const getDefaultSyncTime = () => getDateRange(dayjs(), dayjs())
|
||||
|
||||
const syncTypeOptions = [
|
||||
{ label: '手动', value: 'manual' },
|
||||
{ label: '定时', value: 'cron' }
|
||||
]
|
||||
const syncStatusOptions = [
|
||||
{ label: '成功', value: 'success' },
|
||||
{ label: '失败', value: 'fail' },
|
||||
{ label: '部分成功', value: 'partial' }
|
||||
]
|
||||
|
||||
const historyQuery = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
@@ -1772,22 +1851,56 @@ const historyQuery = reactive({
|
||||
syncStatus: undefined as string | undefined
|
||||
})
|
||||
|
||||
const openSyncHistory = () => {
|
||||
if (!currentConfig.value?.id) {
|
||||
message.warning('请先选择同步绑定并保存配置')
|
||||
return
|
||||
const openSyncHistory = async () => {
|
||||
let tagConfigId = currentConfig.value?.id
|
||||
if (!tagConfigId) {
|
||||
try {
|
||||
const res = await TagConfigApi.getTagConfigByTenant()
|
||||
const data = (res as any)?.data ?? res
|
||||
tagConfigId = data?.id
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
historyQuery.tagConfigId = currentConfig.value.id
|
||||
historyQuery.tagConfigId = tagConfigId ?? undefined
|
||||
historyQuery.syncTime = getDefaultSyncTime()
|
||||
syncHistoryVisible.value = true
|
||||
loadSyncHistory()
|
||||
if (tagConfigId) {
|
||||
loadSyncHistory()
|
||||
} else {
|
||||
syncHistoryList.value = []
|
||||
historyTotal.value = 0
|
||||
message.warning('请先选择同步绑定并保存配置')
|
||||
}
|
||||
}
|
||||
|
||||
const detailMap = ref<Record<number, any[]>>({})
|
||||
const detailLoadingMap = ref<Record<number, boolean>>({})
|
||||
|
||||
const onHistoryExpand = async (row: any, expandedRows: any[]) => {
|
||||
const isExpanded = expandedRows.some((r) => r.id === row.id)
|
||||
if (!isExpanded || detailMap.value[row.id]) return
|
||||
detailLoadingMap.value = { ...detailLoadingMap.value, [row.id]: true }
|
||||
try {
|
||||
const res = await TagSyncDetailApi.getDetailListBySyncHistoryId(row.id)
|
||||
const list = (res as any)?.data ?? res
|
||||
detailMap.value = { ...detailMap.value, [row.id]: Array.isArray(list) ? list : [] }
|
||||
} finally {
|
||||
detailLoadingMap.value = { ...detailLoadingMap.value, [row.id]: false }
|
||||
}
|
||||
}
|
||||
|
||||
const loadSyncHistory = async () => {
|
||||
if (!historyQuery.tagConfigId) return
|
||||
historyLoading.value = true
|
||||
detailMap.value = {}
|
||||
try {
|
||||
const res = await TagSyncHistoryApi.getTagSyncHistoryPage(historyQuery)
|
||||
const params = {
|
||||
...historyQuery,
|
||||
syncType: historyQuery.syncType || undefined,
|
||||
syncStatus: historyQuery.syncStatus || undefined
|
||||
}
|
||||
const res = await TagSyncHistoryApi.getTagSyncHistoryPage(params)
|
||||
const data = (res as any)?.data ?? res
|
||||
syncHistoryList.value = data?.list || []
|
||||
historyTotal.value = data?.total || 0
|
||||
@@ -1804,6 +1917,35 @@ const resetHistoryQuery = () => {
|
||||
loadSyncHistory()
|
||||
}
|
||||
|
||||
/** 执行SQL 查看弹窗 */
|
||||
const sqlViewerVisible = ref(false)
|
||||
const sqlViewerContent = ref('')
|
||||
const sqlViewerTitle = ref('执行SQL')
|
||||
const openSqlViewer = (d: { tagName?: string; sqlExecuted?: string }) => {
|
||||
sqlViewerTitle.value = `执行SQL${d.tagName ? ' - ' + d.tagName : ''}`
|
||||
sqlViewerContent.value = d.sqlExecuted || '-'
|
||||
copySuccessTip.value = false
|
||||
sqlViewerVisible.value = true
|
||||
}
|
||||
const copySuccessTip = ref(false)
|
||||
let copyTipTimer: ReturnType<typeof setTimeout> | null = null
|
||||
const copySqlContent = async () => {
|
||||
const text = sqlViewerContent.value
|
||||
if (!text || text === '-') return
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
copySuccessTip.value = true
|
||||
if (copyTipTimer) clearTimeout(copyTipTimer)
|
||||
copyTipTimer = setTimeout(() => {
|
||||
copySuccessTip.value = false
|
||||
copyTipTimer = null
|
||||
}, 2000)
|
||||
} catch {
|
||||
copySuccessTip.value = false
|
||||
ElMessage.error({ message: '复制失败', zIndex: 9999 })
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadDatabaseList()
|
||||
loadTags()
|
||||
@@ -2221,6 +2363,56 @@ onMounted(() => {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.sync-detail-expand {
|
||||
padding: 12px 24px;
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
.sync-detail-expand .detail-loading {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.sync-detail-expand .sql-executed {
|
||||
margin: 0;
|
||||
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
.sync-detail-expand .sql-executed.clickable {
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
color: var(--el-color-primary);
|
||||
text-decoration: underline;
|
||||
transition: color 0.2s, background 0.2s;
|
||||
}
|
||||
.sync-detail-expand .sql-executed.clickable:hover {
|
||||
color: var(--el-color-primary-light-3);
|
||||
background: var(--el-fill-color);
|
||||
}
|
||||
|
||||
/* 执行SQL 查看弹窗 */
|
||||
.sql-viewer-dialog :deep(.el-dialog__body) {
|
||||
padding-top: 12px;
|
||||
}
|
||||
.sql-viewer-dialog :deep(.el-dialog__footer) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.sql-copy-tip {
|
||||
color: var(--el-color-success);
|
||||
font-size: 14px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.sql-viewer-textarea :deep(textarea) {
|
||||
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.rules-grid {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
Reference in New Issue
Block a user