fix: 将 admin 改为普通目录
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<div class="login flex flex-col">
|
||||
<div class="flex-1 flex items-center justify-center">
|
||||
<div class="login-card flex rounded-md overflow-hidden">
|
||||
<div class="flex-1 h-full hidden md:inline-block">
|
||||
<image-contain :src="config.login_image" :width="400" height="100%" />
|
||||
</div>
|
||||
<div
|
||||
class="login-form bg-body flex flex-col justify-center px-10 py-10 md:w-[400px] w-[375px] flex-none mx-auto">
|
||||
<div class="text-center text-3xl font-medium mb-8">{{ config.web_name }}</div>
|
||||
<el-form ref="formRef" :model="formData" size="large" :rules="rules">
|
||||
<el-form-item prop="account">
|
||||
<el-input v-model="formData.account" placeholder="请输入账号" @keyup.enter="handleEnter">
|
||||
<template #prepend>
|
||||
<icon name="el-icon-User" size="16" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="password">
|
||||
<el-input ref="passwordRef" v-model="formData.password" show-password placeholder="请输入密码"
|
||||
@keyup.enter="handleLogin">
|
||||
<template #prepend>
|
||||
<icon name="el-icon-Lock" size="16" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="mb-5">
|
||||
<el-checkbox v-model="remAccount" label="记住账号"></el-checkbox>
|
||||
</div>
|
||||
<el-button type="primary" size="large" :loading="isLock" @click="lockLogin">
|
||||
登录
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<layout-footer />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance, InputInstance } from 'element-plus'
|
||||
import { computed, onMounted, reactive, ref, shallowRef } from 'vue'
|
||||
|
||||
import { ACCOUNT_KEY } from '@/enums/cacheEnums'
|
||||
import { PageEnum } from '@/enums/pageEnum'
|
||||
import { useLockFn } from '@/hooks/useLockFn'
|
||||
import LayoutFooter from '@/layout/components/footer.vue'
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import cache from '@/utils/cache'
|
||||
|
||||
const passwordRef = shallowRef<InputInstance>()
|
||||
const formRef = shallowRef<FormInstance>()
|
||||
const appStore = useAppStore()
|
||||
const userStore = useUserStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const remAccount = ref(false)
|
||||
const config = computed(() => appStore.config)
|
||||
const formData = reactive({
|
||||
account: '',
|
||||
password: ''
|
||||
})
|
||||
const rules = {
|
||||
account: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入账号',
|
||||
trigger: ['blur']
|
||||
}
|
||||
],
|
||||
password: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入密码',
|
||||
trigger: ['blur']
|
||||
}
|
||||
]
|
||||
}
|
||||
// 回车按键监听
|
||||
const handleEnter = () => {
|
||||
if (!formData.password) {
|
||||
return passwordRef.value?.focus()
|
||||
}
|
||||
handleLogin()
|
||||
}
|
||||
// 登录处理
|
||||
const handleLogin = async () => {
|
||||
await formRef.value?.validate()
|
||||
// 记住账号,缓存
|
||||
cache.set(ACCOUNT_KEY, {
|
||||
remember: remAccount.value,
|
||||
account: remAccount.value ? formData.account : ''
|
||||
})
|
||||
await userStore.login(formData)
|
||||
const {
|
||||
query: { redirect }
|
||||
} = route
|
||||
const path = typeof redirect === 'string' ? redirect : PageEnum.INDEX
|
||||
router.push(path)
|
||||
}
|
||||
const { isLock, lockFn: lockLogin } = useLockFn(handleLogin)
|
||||
|
||||
onMounted(() => {
|
||||
const value = cache.get(ACCOUNT_KEY)
|
||||
if (value?.remember) {
|
||||
remAccount.value = value.remember
|
||||
formData.account = value.account
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.login {
|
||||
background-image: url('./images/login_bg.png');
|
||||
@apply min-h-screen bg-no-repeat bg-center bg-cover;
|
||||
|
||||
.login-card {
|
||||
height: 400px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div class="ai-analysis">
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" :inline="true">
|
||||
<el-form-item class="w-[200px]" label="类型">
|
||||
<el-select v-model="queryParams.type" clearable placeholder="全部">
|
||||
<el-option label="赛事预测" :value="1" />
|
||||
<el-option label="资讯分析" :value="2" />
|
||||
<el-option label="用户可信度" :value="3" />
|
||||
<el-option label="彩票分析" :value="4" />
|
||||
<el-option label="帖子分析" :value="5" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item class="w-[200px]" label="状态">
|
||||
<el-select v-model="queryParams.status" clearable placeholder="全部">
|
||||
<el-option label="成功" :value="1" />
|
||||
<el-option label="失败" :value="2" />
|
||||
<el-option label="处理中" :value="3" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="resetPage">查询</el-button>
|
||||
<el-button @click="resetParams">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
|
||||
<el-table-column label="ID" prop="id" min-width="60" />
|
||||
<el-table-column label="类型" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.type === 1">赛事预测</el-tag>
|
||||
<el-tag v-else-if="row.type === 2" type="success">资讯分析</el-tag>
|
||||
<el-tag v-else-if="row.type === 3" type="warning">用户可信度</el-tag>
|
||||
<el-tag v-else-if="row.type === 4" type="danger">彩票分析</el-tag>
|
||||
<el-tag v-else-if="row.type === 5" type="info">帖子分析</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="关联ID" prop="ref_id" min-width="80" />
|
||||
<el-table-column label="状态" min-width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.status === 1" type="success">成功</el-tag>
|
||||
<el-tag v-else-if="row.status === 2" type="danger">失败</el-tag>
|
||||
<el-tag v-else-if="row.status === 3" type="warning">处理中</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="过期时间" prop="expire_time" min-width="140" />
|
||||
<el-table-column label="创建时间" prop="create_time" min-width="140" />
|
||||
</el-table>
|
||||
<div class="flex justify-end mt-4">
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup name="aiAnalysis">
|
||||
import { aiAnalysisLists } from '@/api/ai'
|
||||
import { usePaging } from '@/hooks/usePaging'
|
||||
|
||||
const queryParams = reactive({ type: '', status: '' })
|
||||
const { pager, getLists, resetPage, resetParams } = usePaging({
|
||||
fetchFun: aiAnalysisLists,
|
||||
params: queryParams
|
||||
})
|
||||
|
||||
getLists()
|
||||
</script>
|
||||
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div class="ai-config">
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
|
||||
<el-table-column label="ID" prop="id" min-width="60" />
|
||||
<el-table-column label="配置名称" prop="name" min-width="150" />
|
||||
<el-table-column label="配置值" prop="value" min-width="250" show-overflow-tooltip />
|
||||
<el-table-column label="备注" prop="remark" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="100" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button v-perms="['ai.aiConfig/edit']" type="primary" link
|
||||
@click="handleEdit(row)">编辑</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="flex justify-end mt-4">
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="showEdit" title="编辑配置" :width="isPromptConfig ? '800px' : '600px'">
|
||||
<el-form ref="editFormRef" :model="editData" label-width="80px">
|
||||
<el-form-item label="名称">
|
||||
<el-input v-model="editData.name" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="editData.remark" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
<el-form-item label="值">
|
||||
<el-input v-model="editData.value" type="textarea" :rows="isPromptConfig ? 15 : 4"
|
||||
placeholder="请输入配置值" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showEdit = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup name="aiConfig">
|
||||
import { aiConfigLists, aiConfigEdit } from '@/api/ai'
|
||||
import { usePaging } from '@/hooks/usePaging'
|
||||
|
||||
const { pager, getLists } = usePaging({ fetchFun: aiConfigLists })
|
||||
|
||||
const showEdit = ref(false)
|
||||
const editData = reactive({ id: 0, name: '', value: '', remark: '' })
|
||||
const isPromptConfig = computed(() => editData.name.includes('prompt'))
|
||||
|
||||
const handleEdit = (row: any) => {
|
||||
Object.assign(editData, { id: row.id, name: row.name, value: row.value, remark: row.remark || '' })
|
||||
showEdit.value = true
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
await aiConfigEdit(editData)
|
||||
showEdit.value = false
|
||||
getLists()
|
||||
}
|
||||
|
||||
getLists()
|
||||
</script>
|
||||
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<div class="kb-config">
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
|
||||
<el-table-column label="ID" prop="id" min-width="60" />
|
||||
<el-table-column label="配置名称" prop="name" min-width="180" />
|
||||
<el-table-column label="配置值" prop="value" min-width="260" show-overflow-tooltip />
|
||||
<el-table-column label="备注" prop="remark" min-width="220" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="100" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button v-perms="['ai.kbConfig/edit']" type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="flex justify-end mt-4">
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="showEdit" title="编辑知识库配置" :width="isPromptConfig ? '800px' : '600px'">
|
||||
<el-form :model="editData" label-width="90px">
|
||||
<el-form-item label="名称">
|
||||
<el-input v-model="editData.name" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="editData.remark" />
|
||||
</el-form-item>
|
||||
<el-form-item label="值">
|
||||
<el-input v-model="editData.value" type="textarea" :rows="isPromptConfig ? 12 : 4" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showEdit = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="kbConfig">
|
||||
import { kbConfigEdit, kbConfigLists } from '@/api/ai'
|
||||
import { usePaging } from '@/hooks/usePaging'
|
||||
|
||||
const { pager, getLists } = usePaging({ fetchFun: kbConfigLists })
|
||||
|
||||
const showEdit = ref(false)
|
||||
const editData = reactive({ id: 0, name: '', value: '', remark: '' })
|
||||
const isPromptConfig = computed(() => editData.name.includes('prompt'))
|
||||
|
||||
const handleEdit = (row: any) => {
|
||||
Object.assign(editData, { id: row.id, name: row.name, value: row.value, remark: row.remark || '' })
|
||||
showEdit.value = true
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
await kbConfigEdit(editData)
|
||||
showEdit.value = false
|
||||
getLists()
|
||||
}
|
||||
|
||||
getLists()
|
||||
</script>
|
||||
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<div class="kb-document">
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-form class="mb-[-16px]" :model="queryParams" :inline="true">
|
||||
<el-form-item class="w-[180px]" label="领域">
|
||||
<el-select v-model="queryParams.domain" clearable placeholder="全部">
|
||||
<el-option label="资讯" value="article" />
|
||||
<el-option label="帖子" value="post" />
|
||||
<el-option label="彩票" value="lottery" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item class="w-[180px]" label="子类型">
|
||||
<el-select v-model="queryParams.subtype" clearable placeholder="全部">
|
||||
<el-option label="article" value="article" />
|
||||
<el-option label="post" value="post" />
|
||||
<el-option label="draw_result" value="draw_result" />
|
||||
<el-option label="ai_history" value="ai_history" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item class="w-[180px]" label="状态">
|
||||
<el-select v-model="queryParams.is_active" clearable placeholder="全部">
|
||||
<el-option label="启用" :value="1" />
|
||||
<el-option label="停用" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item class="w-[220px]" label="关键词">
|
||||
<el-input v-model="queryParams.title" placeholder="标题或关键词" clearable @keyup.enter="resetPage" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="resetPage">查询</el-button>
|
||||
<el-button @click="resetParams">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
|
||||
<el-table-column label="ID" prop="id" min-width="80" />
|
||||
<el-table-column label="领域" prop="domain" min-width="100" />
|
||||
<el-table-column label="子类型" prop="subtype" min-width="110" />
|
||||
<el-table-column label="来源ID" prop="source_id" min-width="100" />
|
||||
<el-table-column label="标题" prop="title" min-width="260" show-overflow-tooltip />
|
||||
<el-table-column label="关键词" prop="keywords" min-width="220" show-overflow-tooltip />
|
||||
<el-table-column label="状态" min-width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.is_active ? 'success' : 'info'">{{ row.is_active ? '启用' : '停用' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="源更新时间" prop="source_updated_at_text" min-width="160" />
|
||||
<el-table-column label="更新时间" prop="update_time_text" min-width="160" />
|
||||
</el-table>
|
||||
<div class="flex justify-end mt-4">
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="kbDocument">
|
||||
import { kbDocumentLists } from '@/api/ai'
|
||||
import { usePaging } from '@/hooks/usePaging'
|
||||
|
||||
const queryParams = reactive({ domain: '', subtype: '', is_active: '', title: '' })
|
||||
const { pager, getLists, resetPage, resetParams } = usePaging({
|
||||
fetchFun: kbDocumentLists,
|
||||
params: queryParams
|
||||
})
|
||||
|
||||
getLists()
|
||||
</script>
|
||||
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<div class="kb-query-log">
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-form class="mb-[-16px]" :model="queryParams" :inline="true">
|
||||
<el-form-item class="w-[180px]" label="场景">
|
||||
<el-input v-model="queryParams.scene" placeholder="输入场景名" clearable @keyup.enter="resetPage" />
|
||||
</el-form-item>
|
||||
<el-form-item class="w-[160px]" label="用户ID">
|
||||
<el-input v-model="queryParams.user_id" placeholder="输入用户ID" clearable @keyup.enter="resetPage" />
|
||||
</el-form-item>
|
||||
<el-form-item class="w-[160px]" label="状态">
|
||||
<el-select v-model="queryParams.status" clearable placeholder="全部">
|
||||
<el-option label="成功" :value="1" />
|
||||
<el-option label="失败" :value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item class="w-[240px]" label="查询词">
|
||||
<el-input v-model="queryParams.query_text" placeholder="输入查询词" clearable @keyup.enter="resetPage" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="resetPage">查询</el-button>
|
||||
<el-button @click="resetParams">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
|
||||
<el-table-column label="ID" prop="id" min-width="70" />
|
||||
<el-table-column label="场景" prop="scene" min-width="140" />
|
||||
<el-table-column label="用户ID" prop="user_id" min-width="90" />
|
||||
<el-table-column label="关联ID" prop="ref_id" min-width="90" />
|
||||
<el-table-column label="查询词" prop="query_text" min-width="280" show-overflow-tooltip />
|
||||
<el-table-column label="Token" prop="tokens_used" min-width="80" />
|
||||
<el-table-column label="耗时(ms)" prop="cost_ms" min-width="90" />
|
||||
<el-table-column label="状态" min-width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'danger'">{{ row.status === 1 ? '成功' : '失败' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" prop="create_time_text" min-width="160" />
|
||||
</el-table>
|
||||
<div class="flex justify-end mt-4">
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="kbQueryLog">
|
||||
import { kbQueryLogLists } from '@/api/ai'
|
||||
import { usePaging } from '@/hooks/usePaging'
|
||||
|
||||
const queryParams = reactive({ scene: '', status: '', user_id: '', query_text: '' })
|
||||
const { pager, getLists, resetPage, resetParams } = usePaging({
|
||||
fetchFun: kbQueryLogLists,
|
||||
params: queryParams
|
||||
})
|
||||
|
||||
getLists()
|
||||
</script>
|
||||
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<div class="kb-sync-job">
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<div class="flex justify-between gap-4 flex-wrap">
|
||||
<el-form class="mb-[-16px]" :model="queryParams" :inline="true">
|
||||
<el-form-item class="w-[160px]" label="领域">
|
||||
<el-select v-model="queryParams.domain" clearable placeholder="全部">
|
||||
<el-option label="资讯" value="article" />
|
||||
<el-option label="帖子" value="post" />
|
||||
<el-option label="彩票" value="lottery" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item class="w-[160px]" label="动作">
|
||||
<el-select v-model="queryParams.action" clearable placeholder="全部">
|
||||
<el-option label="upsert" value="upsert" />
|
||||
<el-option label="delete" value="delete" />
|
||||
<el-option label="rebuild" value="rebuild" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item class="w-[160px]" label="状态">
|
||||
<el-select v-model="queryParams.status" clearable placeholder="全部">
|
||||
<el-option label="待处理" :value="0" />
|
||||
<el-option label="处理中" :value="1" />
|
||||
<el-option label="成功" :value="2" />
|
||||
<el-option label="失败" :value="3" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="resetPage">查询</el-button>
|
||||
<el-button @click="resetParams">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="flex gap-2">
|
||||
<el-button v-perms="['ai.kbSyncJob/rebuild']" @click="runRebuild('article')">重建资讯</el-button>
|
||||
<el-button v-perms="['ai.kbSyncJob/rebuild']" @click="runRebuild('post')">重建帖子</el-button>
|
||||
<el-button v-perms="['ai.kbSyncJob/rebuild']" @click="runRebuild('lottery')">重建彩票</el-button>
|
||||
<el-button v-perms="['ai.kbSyncJob/rebuild']" type="primary" @click="runRebuild('all')">全量重建</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
|
||||
<el-table-column label="ID" prop="id" min-width="80" />
|
||||
<el-table-column label="领域" prop="domain" min-width="90" />
|
||||
<el-table-column label="子类型" prop="subtype" min-width="110" />
|
||||
<el-table-column label="来源ID" prop="source_id" min-width="100" />
|
||||
<el-table-column label="动作" prop="action" min-width="90" />
|
||||
<el-table-column label="优先级" prop="priority" min-width="80" />
|
||||
<el-table-column label="状态" min-width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusTag(row.status)">{{ statusText(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="重试次数" prop="retry_count" min-width="90" />
|
||||
<el-table-column label="计划时间" prop="scheduled_at_text" min-width="160" />
|
||||
<el-table-column label="处理时间" prop="processed_at_text" min-width="160" />
|
||||
<el-table-column label="错误信息" prop="error_msg" min-width="260" show-overflow-tooltip />
|
||||
</el-table>
|
||||
<div class="flex justify-end mt-4">
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="kbSyncJob">
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { kbSyncJobLists, kbSyncJobRebuild } from '@/api/ai'
|
||||
import { usePaging } from '@/hooks/usePaging'
|
||||
|
||||
const queryParams = reactive({ domain: '', action: '', status: '' })
|
||||
const { pager, getLists, resetPage, resetParams } = usePaging({
|
||||
fetchFun: kbSyncJobLists,
|
||||
params: queryParams
|
||||
})
|
||||
|
||||
const runRebuild = async (domain: string) => {
|
||||
await kbSyncJobRebuild({ domain })
|
||||
ElMessage.success('已触发重建任务')
|
||||
getLists()
|
||||
}
|
||||
|
||||
const statusText = (status: number) => {
|
||||
return ['待处理', '处理中', '成功', '失败'][status] || String(status)
|
||||
}
|
||||
|
||||
const statusTag = (status: number) => {
|
||||
return ['info', 'warning', 'success', 'danger'][status] || 'info'
|
||||
}
|
||||
|
||||
getLists()
|
||||
</script>
|
||||
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div class="ai-log">
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" :inline="true">
|
||||
<el-form-item class="w-[200px]" label="用户ID">
|
||||
<el-input v-model="queryParams.user_id" placeholder="输入用户ID" clearable @keyup.enter="resetPage" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="resetPage">查询</el-button>
|
||||
<el-button @click="resetParams">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
|
||||
<el-table-column label="ID" prop="id" min-width="60" />
|
||||
<el-table-column label="用户ID" prop="user_id" min-width="80" />
|
||||
<el-table-column label="类型" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.type === 1">赛事预测</el-tag>
|
||||
<el-tag v-else-if="row.type === 2" type="success">资讯分析</el-tag>
|
||||
<el-tag v-else-if="row.type === 3" type="warning">用户可信度</el-tag>
|
||||
<el-tag v-else-if="row.type === 4" type="danger">彩票分析</el-tag>
|
||||
<el-tag v-else-if="row.type === 5" type="info">帖子分析</el-tag>
|
||||
<el-tag v-else-if="row.type === 6" type="warning">知识库检索</el-tag>
|
||||
<el-tag v-else-if="row.type === 7" type="success">助手对话</el-tag>
|
||||
<el-tag v-else type="info">{{ row.type }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="关联ID" prop="ref_id" min-width="80" />
|
||||
<el-table-column label="Token消耗" prop="tokens_used" min-width="100" />
|
||||
<el-table-column label="耗时(ms)" prop="cost_ms" min-width="100" />
|
||||
<el-table-column label="状态" min-width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.status === 1" type="success">成功</el-tag>
|
||||
<el-tag v-else-if="row.status === 2" type="danger">失败</el-tag>
|
||||
<el-tag v-else type="warning">{{ row.status }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" prop="create_time" min-width="150" />
|
||||
</el-table>
|
||||
<div class="flex justify-end mt-4">
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup name="aiLog">
|
||||
import { aiLogLists } from '@/api/ai'
|
||||
import { usePaging } from '@/hooks/usePaging'
|
||||
|
||||
const queryParams = reactive({ user_id: '' })
|
||||
const { pager, getLists, resetPage, resetParams } = usePaging({
|
||||
fetchFun: aiLogLists,
|
||||
params: queryParams
|
||||
})
|
||||
|
||||
getLists()
|
||||
</script>
|
||||
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card shadow="never" class="!border-none">
|
||||
<template #header>
|
||||
<span class="font-extrabold text-lg">充值设置</span>
|
||||
</template>
|
||||
<el-form :model="formData" label-width="120px">
|
||||
<el-form-item label="状态">
|
||||
<div>
|
||||
<el-radio-group v-model="formData.status" class="ml-4">
|
||||
<el-radio :value="1">开启</el-radio>
|
||||
<el-radio :value="0">关闭</el-radio>
|
||||
</el-radio-group>
|
||||
<div class="form-tips">关闭或开启充值功能,关闭后将不显示充值入口</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="最低充值金额">
|
||||
<div>
|
||||
<el-input
|
||||
v-model="formData.min_amount"
|
||||
placeholder="请输入最低充值金额"
|
||||
clearable
|
||||
/>
|
||||
<div class="form-tips">
|
||||
最低充值金额要求,不填或填0表示不限制最低充值金额
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<footer-btns v-perms="['recharge.recharge/setConfig']">
|
||||
<el-button type="primary" @click="handleSubmit">保存</el-button>
|
||||
</footer-btns>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup name="rechargeConfig">
|
||||
import { getRechargeConfig, setRechargeConfig } from '@/api/app/recharge'
|
||||
|
||||
const formData = reactive({
|
||||
status: 1, //功能状态 1-开启 0-关闭
|
||||
min_amount: '' //最低充值金额
|
||||
})
|
||||
|
||||
const getConfig = async () => {
|
||||
const data = await getRechargeConfig()
|
||||
Object.assign(formData, data)
|
||||
}
|
||||
const handleSubmit = async () => {
|
||||
await setRechargeConfig(formData)
|
||||
getConfig()
|
||||
}
|
||||
getConfig()
|
||||
</script>
|
||||
@@ -0,0 +1,216 @@
|
||||
<template>
|
||||
<div class="app-version-edit">
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-page-header :content="$route.meta.title" @back="$router.back()" />
|
||||
</el-card>
|
||||
<el-card class="mt-4 !border-none" shadow="never">
|
||||
<el-form ref="formRef" class="ls-form" :model="formData" label-width="140px" :rules="rules">
|
||||
<el-form-item label="版本号" prop="version_name">
|
||||
<div class="w-80">
|
||||
<el-input v-model="formData.version_name" placeholder="如 1.0.12" maxlength="32" clearable
|
||||
@input="onVersionNameInput" />
|
||||
<div class="form-tips">填写后会自动计算版本数字</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="版本数字" prop="version_code">
|
||||
<div class="w-80">
|
||||
<el-input-number v-model="formData.version_code" :min="1" :max="999999" class="!w-full" />
|
||||
<div class="form-tips">
|
||||
规则:主×100+次×10+修订;修订≥10时为主×1000+次×100+修订。
|
||||
APP端用此值判断升级
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="平台" prop="platform">
|
||||
<el-radio-group v-model="formData.platform">
|
||||
<el-radio :value="1">Android</el-radio>
|
||||
<el-radio :value="2">iOS</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="包类型" prop="package_type">
|
||||
<el-radio-group v-model="formData.package_type">
|
||||
<el-radio :value="1">wgt热更</el-radio>
|
||||
<el-radio :value="2">apk整包</el-radio>
|
||||
<el-radio :value="3">ipa/AppStore</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="formData.package_type === 1" label="原生最低版本">
|
||||
<div class="w-80">
|
||||
<el-input v-model="minVersionName" placeholder="如 1.0.0(不填表示不限制)" maxlength="32" clearable
|
||||
@input="onMinVersionNameInput" />
|
||||
<div class="form-tips">
|
||||
wgt热更要求的最低原生版本号。
|
||||
APP当前版本低于此值将不推送本次更新(已自动转为版本数字:{{
|
||||
formData.min_version_code
|
||||
}})
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="升级类型" prop="update_type">
|
||||
<el-radio-group v-model="formData.update_type">
|
||||
<el-radio :value="1">普通升级</el-radio>
|
||||
<el-radio :value="2">强制升级</el-radio>
|
||||
<el-radio :value="3">静默升级</el-radio>
|
||||
</el-radio-group>
|
||||
<div class="form-tips ml-3">普通可跳过;强制必须升级;静默后台下载</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="下载地址" prop="download_url">
|
||||
<div class="w-full">
|
||||
<el-input v-model="formData.download_url" placeholder="完整下载链接(wgt/apk直链 或 AppStore链接)"
|
||||
maxlength="500" clearable type="textarea" :autosize="{ minRows: 2, maxRows: 3 }" />
|
||||
<div class="mt-2 flex items-center gap-3">
|
||||
<upload type="file" :limit="1" :multiple="false" show-progress @success="onUploadSuccess">
|
||||
<el-button type="primary" plain>
|
||||
<template #icon>
|
||||
<icon name="el-icon-Upload" />
|
||||
</template>
|
||||
上传 wgt / apk / ipa
|
||||
</el-button>
|
||||
</upload>
|
||||
<span v-if="uploadFileName" class="text-sm text-gray-500">
|
||||
已上传:{{ uploadFileName }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-tips">
|
||||
wgt/apk 可直接上传(最大100MB),也可手动填写完整 https URL;
|
||||
iOS 可填 AppStore 链接(itms-apps:// 或 https://apps.apple.com/...)
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="更新标题" prop="title">
|
||||
<div class="w-80">
|
||||
<el-input v-model="formData.title" placeholder="如 功能优化与bug修复" maxlength="100" show-word-limit
|
||||
clearable />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="更新内容" prop="update_content">
|
||||
<div class="w-[640px]">
|
||||
<el-input v-model="formData.update_content"
|
||||
placeholder="按行书写更新内容,每行一个 例如: 1. 修复登录bug 2. 新增比赛详情" type="textarea"
|
||||
:autosize="{ minRows: 6, maxRows: 12 }" maxlength="2000" show-word-limit />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio :value="1">启用</el-radio>
|
||||
<el-radio :value="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<footer-btns>
|
||||
<el-button type="primary" @click="handleSave">保存</el-button>
|
||||
</footer-btns>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="appVersionEdit">
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { appVersionAdd, appVersionDetail, appVersionEdit } from '@/api/app/version'
|
||||
import useMultipleTabs from '@/hooks/useMultipleTabs'
|
||||
import Upload from '@/components/upload/index.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { removeTab } = useMultipleTabs()
|
||||
const formRef = shallowRef<FormInstance>()
|
||||
|
||||
const formData = reactive({
|
||||
id: '',
|
||||
version_name: '',
|
||||
version_code: 0,
|
||||
min_version_code: 0,
|
||||
platform: 1,
|
||||
package_type: 1,
|
||||
update_type: 1,
|
||||
download_url: '',
|
||||
title: '',
|
||||
update_content: '',
|
||||
status: 1
|
||||
})
|
||||
|
||||
const rules = reactive({
|
||||
version_name: [{ required: true, message: '请输入版本号', trigger: 'blur' }],
|
||||
version_code: [{ required: true, message: '请输入版本数字', trigger: 'blur' }],
|
||||
platform: [{ required: true, message: '请选择平台', trigger: 'change' }],
|
||||
package_type: [{ required: true, message: '请选择包类型', trigger: 'change' }],
|
||||
update_type: [{ required: true, message: '请选择升级类型', trigger: 'change' }],
|
||||
download_url: [{ required: true, message: '请填写下载地址', trigger: 'blur' }],
|
||||
title: [{ required: true, message: '请输入更新标题', trigger: 'blur' }],
|
||||
update_content: [{ required: true, message: '请输入更新内容', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
// 版本号字符串(1.0.12) -> 版本数字(1012)
|
||||
// 规则:主×100+次×10+修订;修订≥10时为主×1000+次×100+修订
|
||||
const versionNameToCode = (value: string): number => {
|
||||
const parts = String(value || '')
|
||||
.split('.')
|
||||
.map((v) => parseInt(v, 10))
|
||||
if (parts.length !== 3 || parts.some((n) => isNaN(n) || n < 0)) return 0
|
||||
const [major, minor, patch] = parts
|
||||
return patch >= 10 ? major * 1000 + minor * 100 + patch : major * 100 + minor * 10 + patch
|
||||
}
|
||||
|
||||
// 版本数字(1012) -> 版本号字符串(1.0.12)
|
||||
const versionCodeToName = (code: number): string => {
|
||||
const c = Number(code) || 0
|
||||
if (c <= 0) return ''
|
||||
if (c >= 1000) {
|
||||
const major = Math.floor(c / 1000)
|
||||
const minor = Math.floor((c % 1000) / 100)
|
||||
const patch = c % 100
|
||||
return `${major}.${minor}.${patch}`
|
||||
}
|
||||
const major = Math.floor(c / 100)
|
||||
const minor = Math.floor((c % 100) / 10)
|
||||
const patch = c % 10
|
||||
return `${major}.${minor}.${patch}`
|
||||
}
|
||||
|
||||
const minVersionName = ref('')
|
||||
const uploadFileName = ref('')
|
||||
|
||||
const onUploadSuccess = (response: any) => {
|
||||
if (response?.code === 1 && response.data) {
|
||||
formData.download_url = response.data.uri || response.data.url || ''
|
||||
uploadFileName.value = response.data.name || ''
|
||||
}
|
||||
}
|
||||
|
||||
const onVersionNameInput = (value: string) => {
|
||||
const code = versionNameToCode(value)
|
||||
if (code > 0) formData.version_code = code
|
||||
}
|
||||
|
||||
const onMinVersionNameInput = (value: string) => {
|
||||
if (!value) {
|
||||
formData.min_version_code = 0
|
||||
return
|
||||
}
|
||||
const code = versionNameToCode(value)
|
||||
formData.min_version_code = code // 0 表示格式不合法,将作为不限制处理
|
||||
}
|
||||
|
||||
const getDetails = async () => {
|
||||
const data = await appVersionDetail({ id: route.query.id })
|
||||
Object.keys(formData).forEach((key) => {
|
||||
// @ts-ignore
|
||||
formData[key] = data[key]
|
||||
})
|
||||
// 回显原生最低版本号字符串
|
||||
minVersionName.value = versionCodeToName(formData.min_version_code)
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
await formRef.value?.validate()
|
||||
if (route.query.id) {
|
||||
await appVersionEdit(formData)
|
||||
} else {
|
||||
await appVersionAdd(formData)
|
||||
}
|
||||
removeTab()
|
||||
router.back()
|
||||
}
|
||||
|
||||
route.query.id && getDetails()
|
||||
</script>
|
||||
@@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<div class="app-version-lists">
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" :inline="true">
|
||||
<el-form-item class="w-[280px]" label="版本号">
|
||||
<el-input
|
||||
v-model="queryParams.version_name"
|
||||
placeholder="如 1.0.12"
|
||||
clearable
|
||||
@keyup.enter="resetPage"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item class="w-[200px]" label="平台">
|
||||
<el-select v-model="queryParams.platform">
|
||||
<el-option label="全部" value />
|
||||
<el-option label="Android" :value="1" />
|
||||
<el-option label="iOS" :value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item class="w-[200px]" label="包类型">
|
||||
<el-select v-model="queryParams.package_type">
|
||||
<el-option label="全部" value />
|
||||
<el-option label="wgt热更" :value="1" />
|
||||
<el-option label="apk整包" :value="2" />
|
||||
<el-option label="ipa/AppStore" :value="3" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item class="w-[200px]" label="升级类型">
|
||||
<el-select v-model="queryParams.update_type">
|
||||
<el-option label="全部" value />
|
||||
<el-option label="普通升级" :value="1" />
|
||||
<el-option label="强制升级" :value="2" />
|
||||
<el-option label="静默升级" :value="3" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item class="w-[200px]" label="状态">
|
||||
<el-select v-model="queryParams.status">
|
||||
<el-option label="全部" value />
|
||||
<el-option label="启用" :value="1" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="resetPage">查询</el-button>
|
||||
<el-button @click="resetParams">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<div class="mb-4 flex items-center gap-3">
|
||||
<router-link
|
||||
v-perms="['app.appVersion/add', 'app.appVersion/add:edit']"
|
||||
:to="{ path: getRoutePath('app.appVersion/add:edit') }"
|
||||
>
|
||||
<el-button type="primary">
|
||||
<template #icon>
|
||||
<icon name="el-icon-Plus" />
|
||||
</template>
|
||||
发布新版本
|
||||
</el-button>
|
||||
</router-link>
|
||||
</div>
|
||||
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
|
||||
<el-table-column label="ID" prop="id" width="70" />
|
||||
<el-table-column label="版本号" prop="version_name" width="110" />
|
||||
<el-table-column label="版本数字" prop="version_code" width="100" />
|
||||
<el-table-column label="平台" prop="platform_text" width="90" />
|
||||
<el-table-column label="包类型" prop="package_type_text" width="120" />
|
||||
<el-table-column label="升级类型" prop="update_type_text" width="100" />
|
||||
<el-table-column label="更新标题" prop="title" min-width="160" show-tooltip-when-overflow />
|
||||
<el-table-column label="下载地址" min-width="220" show-tooltip-when-overflow>
|
||||
<template #default="{ row }">
|
||||
<a :href="row.download_url" target="_blank" class="text-blue-500">
|
||||
{{ row.download_url }}
|
||||
</a>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
v-perms="['app.appVersion/updateStatus']"
|
||||
v-model="row.status"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
@change="changeStatus($event, row.id)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="发布时间" prop="create_time" width="160" />
|
||||
<el-table-column label="操作" width="140" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-perms="['app.appVersion/edit', 'app.appVersion/add:edit']"
|
||||
type="primary"
|
||||
link
|
||||
>
|
||||
<router-link
|
||||
:to="{
|
||||
path: getRoutePath('app.appVersion/add:edit'),
|
||||
query: { id: row.id }
|
||||
}"
|
||||
>
|
||||
编辑
|
||||
</router-link>
|
||||
</el-button>
|
||||
<el-button
|
||||
v-perms="['app.appVersion/delete']"
|
||||
type="danger"
|
||||
link
|
||||
@click="handleDelete(row.id)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="flex justify-end mt-4">
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="appVersionLists">
|
||||
import { appVersionLists, appVersionDelete, appVersionStatus } from '@/api/app/version'
|
||||
import { usePaging } from '@/hooks/usePaging'
|
||||
import { getRoutePath } from '@/router'
|
||||
import feedback from '@/utils/feedback'
|
||||
|
||||
const queryParams = reactive({
|
||||
version_name: '',
|
||||
platform: '',
|
||||
package_type: '',
|
||||
update_type: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
const { pager, getLists, resetPage, resetParams } = usePaging({
|
||||
fetchFun: appVersionLists,
|
||||
params: queryParams
|
||||
})
|
||||
|
||||
const changeStatus = async (status: any, id: number) => {
|
||||
try {
|
||||
await appVersionStatus({ id, status })
|
||||
getLists()
|
||||
} catch {
|
||||
getLists()
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
await feedback.confirm('确定要删除此版本?')
|
||||
await appVersionDelete({ id })
|
||||
getLists()
|
||||
}
|
||||
|
||||
onActivated(() => {
|
||||
getLists()
|
||||
})
|
||||
|
||||
getLists()
|
||||
</script>
|
||||
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<div class="ai-comment-task">
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" :inline="true">
|
||||
<el-form-item class="w-[280px]" label="文章ID">
|
||||
<el-input v-model="queryParams.article_id" placeholder="输入文章ID" clearable @keyup.enter="resetPage" />
|
||||
</el-form-item>
|
||||
<el-form-item class="w-[280px]" label="任务状态">
|
||||
<el-select v-model="queryParams.status" clearable>
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="待执行" :value="0" />
|
||||
<el-option label="已执行" :value="1" />
|
||||
<el-option label="失败" :value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="resetPage">查询</el-button>
|
||||
<el-button @click="resetParams">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
|
||||
<el-table-column label="ID" prop="id" min-width="60" />
|
||||
<el-table-column label="文章ID" prop="article_id" min-width="80" />
|
||||
<el-table-column label="虚拟用户" min-width="120">
|
||||
<template #default="{ row }">
|
||||
{{ row.virtual_nickname }} (ID:{{ row.virtual_user_id }})
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="评论内容" prop="comment_content" min-width="260" show-tooltip-when-overflow />
|
||||
<el-table-column label="计划执行时间" prop="run_at_text" min-width="160" />
|
||||
<el-table-column label="状态" min-width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.status === 0" type="info">待执行</el-tag>
|
||||
<el-tag v-else-if="row.status === 1" type="success">已执行</el-tag>
|
||||
<el-tag v-else-if="row.status === 2" type="danger">失败</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="错误信息" prop="error_msg" min-width="180" show-tooltip-when-overflow />
|
||||
<el-table-column label="创建时间" prop="create_time_text" min-width="160" />
|
||||
<el-table-column label="操作" width="140" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.status === 2" type="primary" link @click="handleRetry(row.id)">
|
||||
重试
|
||||
</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(row.id)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="flex justify-end mt-4">
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive } from 'vue'
|
||||
import {
|
||||
articleAiCommentTaskLists,
|
||||
articleAiCommentTaskDelete,
|
||||
articleAiCommentTaskRetry
|
||||
} from '@/api/article'
|
||||
import { usePaging } from '@/hooks/usePaging'
|
||||
import feedback from '@/utils/feedback'
|
||||
|
||||
const queryParams = reactive({
|
||||
article_id: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
const { pager, getLists, resetPage, resetParams } = usePaging({
|
||||
fetchFun: articleAiCommentTaskLists,
|
||||
params: queryParams
|
||||
})
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
await feedback.confirm('确定要删除该任务?')
|
||||
await articleAiCommentTaskDelete({ id })
|
||||
getLists()
|
||||
}
|
||||
|
||||
const handleRetry = async (id: number) => {
|
||||
await feedback.confirm('确定要重试该任务?任务将重新加入队列。')
|
||||
await articleAiCommentTaskRetry({ id })
|
||||
getLists()
|
||||
}
|
||||
|
||||
getLists()
|
||||
</script>
|
||||
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div class="edit-popup">
|
||||
<popup
|
||||
ref="popupRef"
|
||||
:title="popupTitle"
|
||||
:async="true"
|
||||
width="550px"
|
||||
@confirm="handleSubmit"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form ref="formRef" :model="formData" label-width="84px" :rules="formRules">
|
||||
<el-form-item label="栏目名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入栏目名称" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<div>
|
||||
<el-input-number v-model="formData.sort" :min="0" :max="9999" />
|
||||
<div class="form-tips">默认为0, 数值越大越排前</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="is_show">
|
||||
<el-switch v-model="formData.is_show" :active-value="1" :inactive-value="0" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</popup>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup name="articleColumnEdit">
|
||||
import type { FormInstance } from 'element-plus'
|
||||
|
||||
import { articleCateAdd, articleCateDetail, articleCateEdit } from '@/api/article'
|
||||
import Popup from '@/components/popup/index.vue'
|
||||
|
||||
const emit = defineEmits(['success', 'close'])
|
||||
const formRef = shallowRef<FormInstance>()
|
||||
const popupRef = shallowRef<InstanceType<typeof Popup>>()
|
||||
const mode = ref('add')
|
||||
const popupTitle = computed(() => {
|
||||
return mode.value == 'edit' ? '编辑栏目' : '新增栏目'
|
||||
})
|
||||
const formData = reactive({
|
||||
id: '',
|
||||
name: '',
|
||||
sort: 0,
|
||||
is_show: 1
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
name: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入栏目名称',
|
||||
trigger: ['blur']
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
await formRef.value?.validate()
|
||||
mode.value == 'edit' ? await articleCateEdit(formData) : await articleCateAdd(formData)
|
||||
popupRef.value?.close()
|
||||
emit('success')
|
||||
}
|
||||
|
||||
const open = (type = 'add') => {
|
||||
mode.value = type
|
||||
popupRef.value?.open()
|
||||
}
|
||||
|
||||
const setFormData = (data: Record<any, any>) => {
|
||||
for (const key in formData) {
|
||||
if (data[key] != null && data[key] != undefined) {
|
||||
//@ts-ignore
|
||||
formData[key] = data[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getDetail = async (row: Record<string, any>) => {
|
||||
const data = await articleCateDetail({
|
||||
id: row.id
|
||||
})
|
||||
setFormData(data)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
open,
|
||||
setFormData,
|
||||
getDetail
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card class="!border-none mt-4" shadow="never" v-loading="loading">
|
||||
<div>
|
||||
<el-button class="mb-4" v-perms="['article.articleCate/add']" type="primary" @click="handleAdd()">
|
||||
<template #icon>
|
||||
<icon name="el-icon-Plus" />
|
||||
</template>
|
||||
新增
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table size="large" :data="tableData" row-key="id" :tree-props="{ children: 'children' }"
|
||||
default-expand-all>
|
||||
<el-table-column label="栏目名称" prop="name" min-width="120" />
|
||||
<el-table-column label="文章数" prop="article_count" min-width="120" />
|
||||
<el-table-column label="状态" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<el-switch v-perms="['article.articleCate/updateStatus']" v-model="row.is_show"
|
||||
:active-value="1" :inactive-value="0" @change="changeStatus($event, row.id)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="排序" prop="sort" min-width="120" />
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button v-perms="['article.articleCate/edit']" type="primary" link @click="handleEdit(row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button v-perms="['article.articleCate/delete']" type="danger" link
|
||||
@click="handleDelete(row.id)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup name="articleColumn">
|
||||
import { articleCateDelete, articleCateLists, articleCateStatus } from '@/api/article'
|
||||
import feedback from '@/utils/feedback'
|
||||
|
||||
import EditPopup from './edit.vue'
|
||||
|
||||
const editRef = shallowRef<InstanceType<typeof EditPopup>>()
|
||||
const showEdit = ref(false)
|
||||
const loading = ref(false)
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
const getLists = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await articleCateLists({ page_type: 0 })
|
||||
tableData.value = res?.lists ?? []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleAdd = async () => {
|
||||
showEdit.value = true
|
||||
await nextTick()
|
||||
editRef.value?.open('add')
|
||||
}
|
||||
|
||||
const handleEdit = async (data: any) => {
|
||||
showEdit.value = true
|
||||
await nextTick()
|
||||
editRef.value?.open('edit')
|
||||
editRef.value?.getDetail(data)
|
||||
}
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
await feedback.confirm('确定要删除?')
|
||||
await articleCateDelete({ id })
|
||||
getLists()
|
||||
}
|
||||
|
||||
const changeStatus = async (is_show: any, id: number) => {
|
||||
try {
|
||||
await articleCateStatus({ id, is_show })
|
||||
getLists()
|
||||
} catch (error) {
|
||||
getLists()
|
||||
}
|
||||
}
|
||||
|
||||
getLists()
|
||||
</script>
|
||||
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<div class="article-edit">
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-page-header :content="$route.meta.title" @back="$router.back()" />
|
||||
</el-card>
|
||||
<el-card class="mt-4 !border-none" shadow="never">
|
||||
<el-form ref="formRef" class="ls-form" :model="formData" label-width="85px" :rules="rules">
|
||||
<div class="xl:flex">
|
||||
<div>
|
||||
<el-form-item label="文章标题" prop="title">
|
||||
<div class="w-80">
|
||||
<el-input v-model="formData.title" placeholder="请输入文章标题" type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 3 }" maxlength="64" show-word-limit clearable />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="文章栏目" prop="cid">
|
||||
<el-select class="w-80" v-model="formData.cid" placeholder="请选择文章栏目" clearable>
|
||||
<el-option v-for="item in optionsData.article_cate" :key="item.id" :label="item.name"
|
||||
:value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="文章简介" prop="desc">
|
||||
<div class="w-80">
|
||||
<el-input v-model="formData.desc" placeholder="请输入文章简介" type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 6 }" :maxlength="200" show-word-limit clearable />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="摘要" prop="abstract">
|
||||
<div class="w-80">
|
||||
<el-input type="textarea" :autosize="{ minRows: 6, maxRows: 6 }"
|
||||
v-model="formData.abstract" maxlength="200" show-word-limit clearable />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="文章封面" prop="image">
|
||||
<div>
|
||||
<div>
|
||||
<material-picker v-model="formData.image" :limit="1" />
|
||||
</div>
|
||||
<div class="form-tips">建议尺寸:240*180px</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="作者" prop="author">
|
||||
<div class="w-80">
|
||||
<el-input v-model="formData.author" placeholder="请输入作者名称" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<div>
|
||||
<el-input-number v-model="formData.sort" :min="0" :max="9999" />
|
||||
<div class="form-tips">默认为0, 数值越大越排前</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="初始浏览量" prop="click_virtual">
|
||||
<div>
|
||||
<el-input-number v-model="formData.click_virtual" :min="0" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="文章状态" required prop="is_show">
|
||||
<el-radio-group v-model="formData.is_show">
|
||||
<el-radio :value="1">显示</el-radio>
|
||||
<el-radio :value="0">隐藏</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否推荐">
|
||||
<el-radio-group v-model="formData.is_recommend">
|
||||
<el-radio :value="1">推荐</el-radio>
|
||||
<el-radio :value="0">不推荐</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="xl:ml-20">
|
||||
<el-form-item label="文章内容" prop="content">
|
||||
<editor v-model="formData.content" :height="667" :width="375" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<footer-btns>
|
||||
<el-button type="primary" @click="handleSave">保存</el-button>
|
||||
</footer-btns>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="articleListsEdit">
|
||||
import type { FormInstance } from 'element-plus'
|
||||
|
||||
import { articleAdd, articleCateAll, articleDetail, articleEdit } from '@/api/article'
|
||||
import { useDictOptions } from '@/hooks/useDictOptions'
|
||||
import useMultipleTabs from '@/hooks/useMultipleTabs'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const formData = reactive({
|
||||
id: '',
|
||||
title: '',
|
||||
image: '',
|
||||
cid: '',
|
||||
desc: '',
|
||||
author: '',
|
||||
content: '',
|
||||
click_virtual: 0,
|
||||
sort: 0,
|
||||
is_show: 1,
|
||||
is_recommend: 0,
|
||||
abstract: ''
|
||||
})
|
||||
|
||||
const { removeTab } = useMultipleTabs()
|
||||
const formRef = shallowRef<FormInstance>()
|
||||
const rules = reactive({
|
||||
title: [{ required: true, message: '请输入文章标题', trigger: 'blur' }],
|
||||
cid: [{ required: true, message: '请选择文章栏目', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const getDetails = async () => {
|
||||
const data = await articleDetail({
|
||||
id: route.query.id
|
||||
})
|
||||
Object.keys(formData).forEach((key) => {
|
||||
//@ts-ignore
|
||||
formData[key] = data[key]
|
||||
})
|
||||
}
|
||||
|
||||
const { optionsData } = useDictOptions<{
|
||||
article_cate: any[]
|
||||
}>({
|
||||
article_cate: {
|
||||
api: articleCateAll
|
||||
}
|
||||
})
|
||||
|
||||
const handleSave = async () => {
|
||||
await formRef.value?.validate()
|
||||
if (route.query.id) {
|
||||
await articleEdit(formData)
|
||||
} else {
|
||||
await articleAdd(formData)
|
||||
}
|
||||
removeTab()
|
||||
router.back()
|
||||
}
|
||||
|
||||
route.query.id && getDetails()
|
||||
</script>
|
||||
@@ -0,0 +1,171 @@
|
||||
<template>
|
||||
<div class="article-lists">
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" :inline="true">
|
||||
<el-form-item class="w-[280px]" label="文章标题">
|
||||
<el-input v-model="queryParams.title" placeholder="输入文章标题" clearable @keyup.enter="resetPage" />
|
||||
</el-form-item>
|
||||
<el-form-item class="w-[280px]" label="栏目名称">
|
||||
<el-select v-model="queryParams.cid">
|
||||
<el-option label="全部" value />
|
||||
<el-option v-for="item in optionsData.article_cate" :key="item.id" :label="item.name"
|
||||
:value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item class="w-[280px]" label="是否推荐">
|
||||
<el-select v-model="queryParams.is_recommend">
|
||||
<el-option label="全部" value />
|
||||
<el-option label="推荐" :value="1" />
|
||||
<el-option label="未推荐" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item class="w-[280px]" label="文章状态">
|
||||
<el-select v-model="queryParams.is_show">
|
||||
<el-option label="全部" value />
|
||||
<el-option label="显示" :value="1" />
|
||||
<el-option label="隐藏" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="resetPage">查询</el-button>
|
||||
<el-button @click="resetParams">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<div class="mb-4 flex items-center gap-3">
|
||||
<router-link v-perms="['article.article/add', 'article.article/add:edit']" :to="{
|
||||
path: getRoutePath('article.article/add:edit')
|
||||
}">
|
||||
<el-button type="primary">
|
||||
<template #icon>
|
||||
<icon name="el-icon-Plus" />
|
||||
</template>
|
||||
发布文章
|
||||
</el-button>
|
||||
</router-link>
|
||||
<el-button :disabled="!selectedIds.length" type="warning" @click="handleBatchRecommend(1)">
|
||||
批量推荐
|
||||
</el-button>
|
||||
<el-button :disabled="!selectedIds.length" type="info" @click="handleBatchRecommend(0)">
|
||||
取消推荐
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table size="large" v-loading="pager.loading" :data="pager.lists"
|
||||
@selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column label="ID" prop="id" min-width="80" />
|
||||
<el-table-column label="封面" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<image-contain v-if="row.image" :src="row.image" :width="60" :height="45"
|
||||
:preview-src-list="[row.image]" preview-teleported fit="contain" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="标题" prop="title" min-width="160" show-tooltip-when-overflow />
|
||||
<el-table-column label="栏目" prop="cate_name" min-width="100" />
|
||||
<el-table-column label="作者" prop="author" min-width="120" />
|
||||
<el-table-column label="浏览量" prop="click" min-width="100" />
|
||||
<el-table-column label="状态" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<el-switch v-perms="['article.article/updateStatus']" v-model="row.is_show" :active-value="1"
|
||||
:inactive-value="0" @change="changeStatus($event, row.id)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="推荐" min-width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.is_recommend" type="warning">推荐</el-tag>
|
||||
<el-tag v-else type="info">未推荐</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="排序" prop="sort" min-width="100" />
|
||||
<el-table-column label="发布时间" prop="create_time" min-width="120" />
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button v-perms="['article.article/edit', 'article.article/add:edit']" type="primary" link>
|
||||
<router-link :to="{
|
||||
path: getRoutePath('article.article/add:edit'),
|
||||
query: {
|
||||
id: row.id
|
||||
}
|
||||
}">
|
||||
编辑
|
||||
</router-link>
|
||||
</el-button>
|
||||
<el-button v-perms="['article.article/delete']" type="danger" link
|
||||
@click="handleDelete(row.id)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="flex justify-end mt-4">
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup name="articleLists">
|
||||
import {
|
||||
articleCateAll,
|
||||
articleDelete,
|
||||
articleLists,
|
||||
articleStatus,
|
||||
articleBatchRecommend
|
||||
} from '@/api/article'
|
||||
import { useDictOptions } from '@/hooks/useDictOptions'
|
||||
import { usePaging } from '@/hooks/usePaging'
|
||||
import { getRoutePath } from '@/router'
|
||||
import feedback from '@/utils/feedback'
|
||||
|
||||
const queryParams = reactive({
|
||||
title: '',
|
||||
cid: '',
|
||||
is_show: '',
|
||||
is_recommend: ''
|
||||
})
|
||||
|
||||
const selectedIds = ref<number[]>([])
|
||||
const handleSelectionChange = (rows: any[]) => {
|
||||
selectedIds.value = rows.map((r) => r.id)
|
||||
}
|
||||
const handleBatchRecommend = async (is_recommend: number) => {
|
||||
if (!selectedIds.value.length) return
|
||||
await feedback.confirm(`确定要${is_recommend ? '设为推荐' : '取消推荐'}选中的 ${selectedIds.value.length} 篇文章?`)
|
||||
await articleBatchRecommend({ ids: selectedIds.value, is_recommend })
|
||||
getLists()
|
||||
}
|
||||
|
||||
const { pager, getLists, resetPage, resetParams } = usePaging({
|
||||
fetchFun: articleLists,
|
||||
params: queryParams
|
||||
})
|
||||
|
||||
const { optionsData } = useDictOptions<{
|
||||
article_cate: any[]
|
||||
}>({
|
||||
article_cate: {
|
||||
api: articleCateAll
|
||||
}
|
||||
})
|
||||
|
||||
const changeStatus = async (is_show: any, id: number) => {
|
||||
try {
|
||||
await articleStatus({ id, is_show })
|
||||
getLists()
|
||||
} catch (error) {
|
||||
getLists()
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
await feedback.confirm('确定要删除?')
|
||||
await articleDelete({ id })
|
||||
getLists()
|
||||
}
|
||||
|
||||
onActivated(() => {
|
||||
getLists()
|
||||
})
|
||||
|
||||
getLists()
|
||||
</script>
|
||||
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-alert type="warning" title="温馨提示:H5设置" :closable="false" show-icon />
|
||||
</el-card>
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<el-form ref="formRef" :model="formData" label-width="120px">
|
||||
<el-form-item label="渠道状态" required prop="status">
|
||||
<div>
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio :value="1">开启</el-radio>
|
||||
<el-radio :value="0">关闭</el-radio>
|
||||
</el-radio-group>
|
||||
<div class="form-tips">状态为关闭时,将不对外提供服务,请谨慎操作</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="关闭后访问页面" prop="page_status">
|
||||
<el-radio-group v-model="formData.page_status">
|
||||
<el-radio :value="0">空页面</el-radio>
|
||||
<el-radio :value="1">自定义链接</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="" prop="page_url" v-if="formData.page_status == 1">
|
||||
<div class="w-80">
|
||||
<el-input v-model="formData.page_url" placeholder="请输入完整的url" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="访问链接">
|
||||
<div class="flex-1 min-w-0 break-words">
|
||||
{{ formData.url }}
|
||||
<el-button v-copy="formData.url">复制</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<footer-btns v-perms="['channel.web_page_setting/setConfig']">
|
||||
<el-button type="primary" @click="handelSave">保存</el-button>
|
||||
</footer-btns>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup name="h5Config">
|
||||
import { getH5Config, setH5Config } from '@/api/channel/h5'
|
||||
|
||||
const formData = reactive({
|
||||
status: 0,
|
||||
page_status: 0,
|
||||
page_url: '',
|
||||
url: ''
|
||||
})
|
||||
|
||||
const getDetail = async () => {
|
||||
const data = await getH5Config()
|
||||
for (const key in formData) {
|
||||
//@ts-ignore
|
||||
formData[key] = data[key]
|
||||
}
|
||||
}
|
||||
|
||||
const handelSave = async () => {
|
||||
await setH5Config(formData)
|
||||
getDetail()
|
||||
}
|
||||
|
||||
getDetail()
|
||||
</script>
|
||||
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-alert
|
||||
type="warning"
|
||||
title="温馨提示:填写微信开放平台开发配置,请前往微信开放平台创建应用并完成认证;网站应用配置主要用于网站微信登录和微信支付"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
</el-card>
|
||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="160px">
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<div class="font-medium mb-7">网站应用</div>
|
||||
<el-form-item label="AppID" prop="app_id">
|
||||
<div class="w-80">
|
||||
<el-input v-model="formData.app_id" placeholder="请输入AppID" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="AppSecret" prop="app_secret">
|
||||
<div>
|
||||
<div class="w-80">
|
||||
<el-input v-model="formData.app_secret" placeholder="请输入AppSecret" />
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
</el-form>
|
||||
<footer-btns v-perms="['channel.open_setting/setConfig']">
|
||||
<el-button type="primary" @click="handelSave">保存</el-button>
|
||||
</footer-btns>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup name="wxDevConfig">
|
||||
import type { FormInstance } from 'element-plus'
|
||||
|
||||
import { getOpenSettingConfig, setOpenSettingConfig } from '@/api/channel/open_setting'
|
||||
|
||||
const formData = reactive({
|
||||
app_id: '',
|
||||
app_secret: ''
|
||||
})
|
||||
|
||||
const formRef = shallowRef<FormInstance>()
|
||||
const formRules = {
|
||||
app_id: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入AppID',
|
||||
trigger: ['blur', 'change']
|
||||
}
|
||||
],
|
||||
app_secret: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入AppSecret',
|
||||
trigger: ['blur', 'change']
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const getDetail = async () => {
|
||||
const data = await getOpenSettingConfig()
|
||||
for (const key in formData) {
|
||||
//@ts-ignore
|
||||
formData[key] = data[key]
|
||||
}
|
||||
}
|
||||
|
||||
const handelSave = async () => {
|
||||
await formRef.value?.validate()
|
||||
await setOpenSettingConfig(formData)
|
||||
getDetail()
|
||||
}
|
||||
|
||||
getDetail()
|
||||
</script>
|
||||
@@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-alert
|
||||
type="warning"
|
||||
title="温馨提示:填写微信小程序开发配置,请前往微信公众平台申请小程序并完成认证"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
</el-card>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
:label-width="appStore.isMobile ? '80px' : '160px'"
|
||||
>
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<div class="font-medium mb-7">微信小程序</div>
|
||||
<el-form-item label="小程序名称" prop="name">
|
||||
<div class="w-80">
|
||||
<el-input v-model="formData.name" placeholder="请输入小程序名称" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="原始ID" prop="original_id">
|
||||
<div class="w-80">
|
||||
<el-input v-model="formData.original_id" placeholder="请输入原始ID" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="小程序码" prop="qr_code">
|
||||
<div class="flex-1">
|
||||
<div>
|
||||
<material-picker v-model="formData.qr_code" :limit="1" />
|
||||
</div>
|
||||
<div class="form-tips">建议尺寸:宽400px*高400px。jpg,jpeg,png格式</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<div class="font-medium mb-7">开发者ID</div>
|
||||
<el-form-item label="AppID" prop="app_id">
|
||||
<div class="w-80">
|
||||
<el-input v-model="formData.app_id" placeholder="请输入AppID" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="AppSecret" prop="app_secret">
|
||||
<div class="w-80">
|
||||
<el-input v-model="formData.app_secret" placeholder="请输入AppSecret" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<div class="form-tips">
|
||||
小程序账号登录微信公众平台,点击开发>开发设置->开发者ID,设置AppID和AppSecret
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<div class="font-medium mb-7">服务器域名</div>
|
||||
<el-form-item label="request合法域名" prop="appId">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="sm:flex">
|
||||
<div class="mr-4 sm:w-80 flex">
|
||||
<el-input v-model="formData.request_domain" disabled />
|
||||
</div>
|
||||
<el-button v-copy="formData.request_domain">复制</el-button>
|
||||
</div>
|
||||
<div class="form-tips">
|
||||
小程序账号登录微信公众平台,点击开发>开发设置->服务器域名,填写https协议域名
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="socket合法域名">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="sm:flex">
|
||||
<div class="mr-4 sm:w-80 flex">
|
||||
<el-input v-model="formData.socket_domain" disabled />
|
||||
</div>
|
||||
<el-button v-copy="formData.socket_domain">复制</el-button>
|
||||
</div>
|
||||
<div class="form-tips">
|
||||
小程序账号登录微信公众平台,点击开发>开发设置->服务器域名,填写wss协议域名
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="uploadFile合法域名">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="sm:flex">
|
||||
<div class="mr-4 sm:w-80 flex">
|
||||
<el-input v-model="formData.upload_file_domain" disabled />
|
||||
</div>
|
||||
<el-button v-copy="formData.upload_file_domain">复制</el-button>
|
||||
</div>
|
||||
<div class="form-tips">
|
||||
小程序账号登录微信公众平台,点击开发>开发设置->服务器域名,填写https协议域名
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="downloadFile合法域名">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="sm:flex">
|
||||
<div class="mr-4 sm:w-80 flex">
|
||||
<el-input v-model="formData.download_file_domain" disabled />
|
||||
</div>
|
||||
<el-button v-copy="formData.download_file_domain">复制</el-button>
|
||||
</div>
|
||||
<div class="form-tips">
|
||||
小程序账号登录微信公众平台,点击开发>开发设置->服务器域名,填写https协议域名
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="udp合法域名">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="sm:flex">
|
||||
<div class="mr-4 sm:w-80 flex">
|
||||
<el-input v-model="formData.udp_domain" disabled />
|
||||
</div>
|
||||
<el-button v-copy="formData.udp_domain">复制</el-button>
|
||||
</div>
|
||||
<div class="form-tips">
|
||||
小程序账号登录微信公众平台,点击开发>开发设置->服务器域名,填写udp协议域名
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<div class="font-medium mb-7">业务域名</div>
|
||||
<el-form-item label="业务域名">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="sm:flex">
|
||||
<div class="mr-4 sm:w-80 flex">
|
||||
<el-input v-model="formData.business_domain" disabled />
|
||||
</div>
|
||||
<el-button v-copy="formData.business_domain">复制</el-button>
|
||||
</div>
|
||||
<div class="form-tips">
|
||||
小程序账号登录微信公众平台,点击开发>开发设置->业务域名,填写业务域名
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
</el-form>
|
||||
<footer-btns v-perms="['channel.mnp_settings/setConfig']">
|
||||
<el-button type="primary" @click="handelSave">保存</el-button>
|
||||
</footer-btns>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup name="weappConfig">
|
||||
import type { FormInstance } from 'element-plus'
|
||||
|
||||
import { getWeappConfig, setWeappConfig } from '@/api/channel/weapp'
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const formData = reactive({
|
||||
name: '',
|
||||
original_id: '',
|
||||
qr_code: '',
|
||||
app_id: '',
|
||||
app_secret: '',
|
||||
business_domain: '',
|
||||
download_file_domain: '',
|
||||
request_domain: '',
|
||||
socket_domain: '',
|
||||
tcpDomain: '',
|
||||
udp_domain: '',
|
||||
upload_file_domain: ''
|
||||
})
|
||||
const formRef = shallowRef<FormInstance>()
|
||||
const formRules = {
|
||||
app_id: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入AppID',
|
||||
trigger: ['blur', 'change']
|
||||
}
|
||||
],
|
||||
app_secret: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入AppSecret',
|
||||
trigger: ['blur', 'change']
|
||||
}
|
||||
]
|
||||
}
|
||||
const getDetail = async () => {
|
||||
const data = await getWeappConfig()
|
||||
for (const key in formData) {
|
||||
//@ts-ignore
|
||||
formData[key] = data[key]
|
||||
}
|
||||
}
|
||||
|
||||
const handelSave = async () => {
|
||||
await formRef.value?.validate()
|
||||
await setWeappConfig(formData)
|
||||
getDetail()
|
||||
}
|
||||
|
||||
getDetail()
|
||||
</script>
|
||||
@@ -0,0 +1,217 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-alert
|
||||
type="warning"
|
||||
title="温馨提示:填写微信公众号开发配置,请前往微信公众平台申请服务号并完成认证"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
</el-card>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
:label-width="appStore.isMobile ? '80px' : '160px'"
|
||||
>
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<div class="font-medium mb-7">微信公众号</div>
|
||||
<el-form-item label="公众号名称" prop="name">
|
||||
<div class="w-80">
|
||||
<el-input v-model="formData.name" placeholder="请输入公众号名称" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="原始ID" prop="original_id">
|
||||
<div class="w-80">
|
||||
<el-input v-model="formData.original_id" placeholder="请输入原始ID" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="公众号二维码" prop="qr_code">
|
||||
<div>
|
||||
<div>
|
||||
<material-picker v-model="formData.qr_code" :limit="1" />
|
||||
</div>
|
||||
<div class="form-tips">建议尺寸:宽400px*高400px。jpg,jpeg,png格式</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<div class="font-medium mb-7">公众号开发者信息</div>
|
||||
<el-form-item label="AppID" prop="app_id">
|
||||
<div class="w-80">
|
||||
<el-input v-model="formData.app_id" placeholder="请输入AppID" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="AppSecret" prop="app_secret">
|
||||
<div class="w-80">
|
||||
<el-input v-model="formData.app_secret" placeholder="请输入AppSecret" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<div class="form-tips">
|
||||
小程序账号登录微信公众平台,点击开发>开发设置->开发者ID,设置AppID和AppSecret
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<div class="font-medium mb-7">服务器配置</div>
|
||||
<el-form-item label="URL">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="sm:flex">
|
||||
<div class="mr-4 sm:w-80 flex">
|
||||
<el-input v-model="formData.url" disabled />
|
||||
</div>
|
||||
<el-button v-copy="formData.url">复制</el-button>
|
||||
</div>
|
||||
<div class="form-tips">
|
||||
登录微信公众平台,点击开发>基本配置>服务器配置,填写服务器地址(URL)
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="Token" prop="Token">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="w-80">
|
||||
<el-input v-model="formData.token" placeholder="请输入Token" />
|
||||
</div>
|
||||
<div class="form-tips">
|
||||
登录微信公众平台,点击开发>基本配置>服务器配置,设置令牌Token。不填默认为“likeshop”
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="EncodingAESKey" prop="encoding_aes_key">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="w-80">
|
||||
<el-input
|
||||
v-model="formData.encoding_aes_key"
|
||||
placeholder="请输入EncodingAESKey"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-tips">
|
||||
消息加密密钥由43位字符组成,字符范围为A-Z,a-z,0-9
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="消息加密方式" required prop="encryption_type">
|
||||
<div class="flex-1 min-w-0">
|
||||
<el-radio-group
|
||||
class="flex-col !items-start min-w-0"
|
||||
v-model="formData.encryption_type"
|
||||
>
|
||||
<el-radio :value="1">
|
||||
明文模式 (不使用消息体加解密功能,安全系数较低)
|
||||
</el-radio>
|
||||
|
||||
<el-radio :value="2">
|
||||
兼容模式 (明文、密文将共存,方便开发者调试和维护)
|
||||
</el-radio>
|
||||
<el-radio :value="3">
|
||||
安全模式(推荐) (消息包为纯密文,需要开发者加密和解密,安全系数高)
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<div class="font-medium mb-7">功能设置</div>
|
||||
<el-form-item label="业务域名">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="sm:flex">
|
||||
<div class="mr-4 sm:w-80 flex">
|
||||
<el-input v-model="formData.business_domain" disabled />
|
||||
</div>
|
||||
<el-button v-copy="formData.business_domain">复制</el-button>
|
||||
</div>
|
||||
<div class="form-tips">
|
||||
登录微信公众平台,点击设置>公众号设置>功能设置,填写业务域名
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="JS接口安全域名">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="sm:flex">
|
||||
<div class="mr-4 sm:w-80 flex">
|
||||
<el-input v-model="formData.js_secure_domain" disabled />
|
||||
</div>
|
||||
<el-button v-copy="formData.js_secure_domain">复制</el-button>
|
||||
</div>
|
||||
<div class="form-tips">
|
||||
登录微信公众平台,点击设置>公众号设置>功能设置,填写JS接口安全域名
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="网页授权域名">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="sm:flex">
|
||||
<div class="mr-4 sm:w-80 flex">
|
||||
<el-input v-model="formData.web_auth_domain" disabled />
|
||||
</div>
|
||||
<el-button v-copy="formData.web_auth_domain">复制</el-button>
|
||||
</div>
|
||||
<div class="form-tips">
|
||||
登录微信公众平台,点击设置>公众号设置>功能设置,填写网页授权域名
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
</el-form>
|
||||
<footer-btns v-perms="['channel.official_account_setting/setConfig']">
|
||||
<el-button type="primary" @click="handelSave">保存</el-button>
|
||||
</footer-btns>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup name="wxOaConfig">
|
||||
import type { FormInstance } from 'element-plus'
|
||||
|
||||
import { getOaConfig, setOaConfig } from '@/api/channel/wx_oa'
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const formData = reactive({
|
||||
name: '',
|
||||
original_id: ' ',
|
||||
qr_code: '',
|
||||
app_id: '',
|
||||
app_secret: '',
|
||||
url: '',
|
||||
token: '',
|
||||
encoding_aes_key: '',
|
||||
encryption_type: 1,
|
||||
business_domain: '',
|
||||
js_secure_domain: '',
|
||||
web_auth_domain: ''
|
||||
})
|
||||
|
||||
const formRef = shallowRef<FormInstance>()
|
||||
const formRules = {
|
||||
app_id: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入AppID',
|
||||
trigger: ['blur', 'change']
|
||||
}
|
||||
],
|
||||
app_secret: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入AppSecret',
|
||||
trigger: ['blur', 'change']
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const getDetail = async () => {
|
||||
const data = await getOaConfig()
|
||||
for (const key in formData) {
|
||||
//@ts-ignore
|
||||
formData[key] = data[key]
|
||||
}
|
||||
}
|
||||
|
||||
const handelSave = async () => {
|
||||
await formRef.value?.validate()
|
||||
await setOaConfig(formData)
|
||||
getDetail()
|
||||
}
|
||||
|
||||
getDetail()
|
||||
</script>
|
||||
@@ -0,0 +1,47 @@
|
||||
<script setup lang="ts" name="wxOaMenu">
|
||||
import OaAttr from './menu_com/oa-attr.vue'
|
||||
import OaPhone from './menu_com/oa-phone.vue'
|
||||
import { useMenuOa } from './menu_com/useMenuOa'
|
||||
|
||||
const { getOaMenuFunc, handleSave, handlePublish } = useMenuOa(undefined)
|
||||
getOaMenuFunc()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="menu-oa">
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-alert
|
||||
type="warning"
|
||||
title="配置微信公众号菜单,点击确认,保存菜单并发布至微信公众号"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<div class="lg:flex flex-1">
|
||||
<!-- Phone -->
|
||||
<oa-phone></oa-phone>
|
||||
|
||||
<!-- Attr -->
|
||||
<div class="mt-4 lg:mt-0 max-w-[400px]">
|
||||
<oa-attr></oa-attr>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<footer-btns>
|
||||
<el-button type="primary" @click="handleSave" v-perms="['channel:oaMenu:save']">
|
||||
保存
|
||||
</el-button>
|
||||
<el-button type="primary" @click="handlePublish" v-perms="['channel:oaMenu:publish']">
|
||||
发布
|
||||
</el-button>
|
||||
</footer-btns>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.menu-oa {
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,90 @@
|
||||
<script lang="ts" setup>
|
||||
import oaMenuForm from './oa-menu-form.vue'
|
||||
import oaMenuFormEdit from './oa-menu-form-edit.vue'
|
||||
import { useMenuOa } from './useMenuOa'
|
||||
|
||||
const menuRef = shallowRef()
|
||||
|
||||
const {
|
||||
menuList,
|
||||
menuIndex,
|
||||
handleAddSubMenu,
|
||||
handleEditSubMenu,
|
||||
handleDelMenu,
|
||||
handleDelSubMenu
|
||||
} = useMenuOa(menuRef)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Attr -->
|
||||
<template v-for="(attrItem, attrIndex) in menuList" :key="attrIndex">
|
||||
<div class="flex-1 oa-attr min-w-0" v-show="attrIndex === menuIndex">
|
||||
<div class="text-base oa-attr-title">菜单配置</div>
|
||||
|
||||
<del-wrap @close="handleDelMenu(menuIndex)">
|
||||
<div class="flex items-center w-full p-4 mt-4 rounded bg-fill-light">
|
||||
<oa-menu-form
|
||||
ref="menuRef"
|
||||
modular="master"
|
||||
v-model:name="attrItem.name"
|
||||
v-model:menuType="attrItem.has_menu"
|
||||
v-model:visitType="attrItem.type"
|
||||
v-model:url="attrItem.url"
|
||||
v-model:appId="attrItem.appid"
|
||||
v-model:pagePath="attrItem.pagepath"
|
||||
>
|
||||
<div class="flex-1">
|
||||
<!-- 编辑子菜单 -->
|
||||
<ul>
|
||||
<li
|
||||
class="flex"
|
||||
v-for="(subItem, subIndex) in attrItem.sub_button"
|
||||
:key="subIndex"
|
||||
style="padding: 8px"
|
||||
>
|
||||
<span class="mr-auto">{{ subItem.name }}</span>
|
||||
<!-- 编辑子菜单 -->
|
||||
<oa-menu-form-edit
|
||||
modular="edit"
|
||||
:subItem="subItem"
|
||||
@edit="handleEditSubMenu($event, subIndex)"
|
||||
>
|
||||
<el-button link>
|
||||
<el-icon><EditPen /></el-icon>
|
||||
</el-button>
|
||||
</oa-menu-form-edit>
|
||||
|
||||
<!-- 删除子菜单 -->
|
||||
<popup @confirm="handleDelSubMenu(menuIndex, subIndex)">
|
||||
是否删除当前子菜单?
|
||||
<template #trigger>
|
||||
<el-button link>
|
||||
<el-icon class="ml-5"><Delete /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</popup>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- 新增子菜单 -->
|
||||
<oa-menu-form-edit modular="add" @add="handleAddSubMenu">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
:disabled="attrItem.sub_button.length >= 5"
|
||||
>
|
||||
添加子菜单({{ attrItem.sub_button.length }}/5)
|
||||
</el-button>
|
||||
</oa-menu-form-edit>
|
||||
</div>
|
||||
</oa-menu-form>
|
||||
</div>
|
||||
</del-wrap>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.oa-attr {
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,73 @@
|
||||
<script lang="ts" setup>
|
||||
import oaMenuForm from './oa-menu-form.vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'add', value: any): void
|
||||
(event: 'edit', value: any): void
|
||||
}>()
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modular: string
|
||||
subItem?: any
|
||||
}>(),
|
||||
{
|
||||
modular: 'edit',
|
||||
subItem: {}
|
||||
}
|
||||
)
|
||||
|
||||
const menuFormEditRef = shallowRef()
|
||||
const menuFromPopupRef = shallowRef()
|
||||
|
||||
const form = {
|
||||
name: '',
|
||||
type: 'view',
|
||||
url: '',
|
||||
appid: '',
|
||||
pagepath: ''
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
if (Object.keys(props.subItem).length != 0) {
|
||||
for (const key in form) {
|
||||
//@ts-ignore
|
||||
form[key] = props.subItem[key]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const handleRules = async () => {
|
||||
await menuFormEditRef.value.menuFormRef.validate()
|
||||
if (props.modular === 'edit') {
|
||||
emit('edit', { ...form })
|
||||
} else {
|
||||
emit('add', { ...form })
|
||||
}
|
||||
menuFromPopupRef.value.close()
|
||||
menuFormEditRef.value.menuFormRef.resetFields()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<popup
|
||||
ref="menuFromPopupRef"
|
||||
async
|
||||
:clickModalClose="false"
|
||||
:title="`${modular === 'add' ? '新增' : '编辑'}子菜单`"
|
||||
@confirm="handleRules"
|
||||
>
|
||||
<oa-menu-form
|
||||
ref="menuFormEditRef"
|
||||
modular="secondary"
|
||||
v-model:name="form.name"
|
||||
v-model:visitType="form.type"
|
||||
v-model:url="form.url"
|
||||
v-model:appId="form.appid"
|
||||
v-model:pagePath="form.pagepath"
|
||||
></oa-menu-form>
|
||||
<template #trigger>
|
||||
<slot></slot>
|
||||
</template>
|
||||
</popup>
|
||||
</template>
|
||||
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<el-form ref="menuFormRef" :rules="rules" :model="menuForm" label-width="100px">
|
||||
<!-- 菜单名称 -->
|
||||
<el-form-item :label="modular === 'master' ? '主菜单名称' : '子菜单名称'" prop="name">
|
||||
<el-input v-model="menuForm.name" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 菜单类型 -->
|
||||
<el-form-item label="主菜单类型" prop="menuType" v-if="modular === 'master'">
|
||||
<el-radio-group v-model="menuForm.menuType">
|
||||
<el-radio :value="false">不配置子菜单</el-radio>
|
||||
<el-radio :value="true">配置子菜单</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="" v-if="menuForm.menuType && modular === 'master'">
|
||||
<slot></slot>
|
||||
</el-form-item>
|
||||
|
||||
<template v-if="!menuForm.menuType">
|
||||
<!-- 跳转链接 -->
|
||||
<el-form-item label="跳转链接" prop="visitType">
|
||||
<el-radio-group v-model="menuForm.visitType">
|
||||
<el-radio value="view">网页</el-radio>
|
||||
<el-radio value="miniprogram">小程序</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 网址 -->
|
||||
<el-form-item label="网址" prop="url">
|
||||
<el-input v-model="menuForm.url" />
|
||||
</el-form-item>
|
||||
|
||||
<template v-if="menuForm.visitType == 'miniprogram'">
|
||||
<!-- AppId -->
|
||||
<el-form-item label="AppId" prop="appId">
|
||||
<el-input v-model="menuForm.appId" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 路径 -->
|
||||
<el-form-item label="路径" prop="pagePath">
|
||||
<el-input v-model="menuForm.pagePath" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
</template>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'element-plus'
|
||||
|
||||
import { rules } from './useMenuOa'
|
||||
|
||||
const emit = defineEmits([
|
||||
'update:name',
|
||||
'update:menuType',
|
||||
'update:visitType',
|
||||
'update:url',
|
||||
'update:appId',
|
||||
'update:pagePath'
|
||||
])
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modular?: string
|
||||
name?: string
|
||||
menuType?: boolean
|
||||
visitType?: string
|
||||
url?: string
|
||||
appId?: string
|
||||
pagePath?: string
|
||||
}>(),
|
||||
{
|
||||
modular: 'master',
|
||||
name: '',
|
||||
menuType: false,
|
||||
visitType: 'view',
|
||||
url: '',
|
||||
appId: '',
|
||||
pagePath: ''
|
||||
}
|
||||
)
|
||||
|
||||
const menuFormRef = shallowRef<FormInstance>()
|
||||
// 表单数据
|
||||
const menuForm = ref({ ...props })
|
||||
|
||||
watch(
|
||||
() => props,
|
||||
(value) => {
|
||||
menuForm.value = value
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.modular === 'master') {
|
||||
emit('update:menuType', menuForm.value.menuType)
|
||||
}
|
||||
emit('update:name', menuForm.value.name)
|
||||
emit('update:visitType', menuForm.value.visitType)
|
||||
emit('update:url', menuForm.value.url)
|
||||
emit('update:appId', menuForm.value.appId)
|
||||
emit('update:pagePath', menuForm.value.pagePath)
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
menuFormRef
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,122 @@
|
||||
<script lang="ts" setup>
|
||||
import useSettingStore from '@/stores/modules/setting'
|
||||
|
||||
import { useMenuOa } from './useMenuOa'
|
||||
|
||||
// 菜单颜色(这里采用全局颜色)
|
||||
const settingStore = useSettingStore()
|
||||
const themeColor = computed(() => settingStore.theme || '#4A5DFF')
|
||||
|
||||
const { menuList, menuIndex, handleAddMenu } = useMenuOa(useMenuOa)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Phone -->
|
||||
<div class="oa-phone mr-[35px]">
|
||||
<div class="oa-phone-content"></div>
|
||||
|
||||
<div class="flex oa-phone-menu">
|
||||
<div class="flex items-center justify-center oa-phone-menu-switch">
|
||||
<el-icon>
|
||||
<Grid />
|
||||
</el-icon>
|
||||
</div>
|
||||
|
||||
<template v-for="(menuItem, i) in menuList" :key="i">
|
||||
<div class="relative flex-1" @click="menuIndex = i">
|
||||
<!-- 一级菜单 -->
|
||||
<div
|
||||
class="flex items-center justify-center flex-1 text-sm oa-phone-menu-item"
|
||||
:class="{ 'active-menu': menuIndex === i }"
|
||||
>
|
||||
{{ menuItem.name }}
|
||||
</div>
|
||||
|
||||
<!-- 二级菜单 -->
|
||||
<div
|
||||
class="oa-phone-menu-subitem"
|
||||
v-show="menuItem.sub_button.length && menuItem.has_menu"
|
||||
>
|
||||
<template v-for="(subItem, index2) in menuItem.sub_button" :key="index2">
|
||||
<div class="oa-phone-menu-subitem-title">
|
||||
{{ subItem.name }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 新增菜单 -->
|
||||
<template v-if="menuList.length <= 2">
|
||||
<div class="flex items-center justify-center flex-1 h-full" @click="handleAddMenu">
|
||||
<el-icon>
|
||||
<Plus />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.oa-phone {
|
||||
width: 260px;
|
||||
height: 461px;
|
||||
border: 1px solid #e5e5ea;
|
||||
flex: none;
|
||||
&-content {
|
||||
height: 420px;
|
||||
border-bottom: 1px solid #e5e5ea;
|
||||
}
|
||||
|
||||
&-menu {
|
||||
height: 40px;
|
||||
cursor: pointer;
|
||||
|
||||
&-switch {
|
||||
width: 40px;
|
||||
height: 100%;
|
||||
border-right: 1px solid #e5e5ea;
|
||||
}
|
||||
|
||||
// 一级菜单
|
||||
&-item {
|
||||
height: 100%;
|
||||
border-right: 1px solid #e5e5ea;
|
||||
}
|
||||
|
||||
&-item:nth-child(4) {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.active-menu {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.active-menu::after {
|
||||
content: '';
|
||||
width: 100%;
|
||||
height: 41px;
|
||||
top: -1px;
|
||||
position: absolute;
|
||||
border: 1px solid v-bind(themeColor);
|
||||
}
|
||||
|
||||
// 二级菜单
|
||||
&-subitem {
|
||||
width: 98%;
|
||||
left: 2px;
|
||||
bottom: calc(100% + 10px);
|
||||
position: absolute;
|
||||
border: 1px solid #e5e5ea;
|
||||
border-top: none;
|
||||
|
||||
&-title {
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
text-align: center;
|
||||
border-top: 1px solid #e5e5ea;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,167 @@
|
||||
import type { FormRules } from 'element-plus'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import type { Menu } from '@/api/channel/wx_oa'
|
||||
import { getOaMenu, setOaMenuPublish, setOaMenuSave } from '@/api/channel/wx_oa'
|
||||
import feedback from '@/utils/feedback'
|
||||
|
||||
// 菜单实例
|
||||
export const menuRef = shallowRef()
|
||||
// 菜单数据
|
||||
const menuList = ref<Menu[]>([])
|
||||
const menuIndex = ref<number>(0)
|
||||
|
||||
// 校验
|
||||
export const rules = reactive<FormRules>({
|
||||
name: [
|
||||
{
|
||||
required: true,
|
||||
message: '必填项不能为空',
|
||||
trigger: ['blur', 'change']
|
||||
},
|
||||
{
|
||||
min: 1,
|
||||
max: 12,
|
||||
message: '长度限制12个字符',
|
||||
trigger: ['blur', 'change']
|
||||
}
|
||||
],
|
||||
menuType: [
|
||||
{
|
||||
required: true,
|
||||
message: '必填项不能为空',
|
||||
trigger: ['blur', 'change']
|
||||
}
|
||||
],
|
||||
visitType: [
|
||||
{
|
||||
required: true,
|
||||
message: '必填项不能为空',
|
||||
trigger: ['blur', 'change']
|
||||
}
|
||||
],
|
||||
url: [
|
||||
{
|
||||
required: true,
|
||||
message: '必填项不能为空',
|
||||
trigger: ['blur', 'change']
|
||||
},
|
||||
{
|
||||
pattern:
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
/(http|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&:/~\+#]*[\w\-\@?^=%&/~\+#])?/,
|
||||
message: '请输入合法的网址链接',
|
||||
trigger: ['blur', 'change']
|
||||
}
|
||||
],
|
||||
appId: [
|
||||
{
|
||||
required: true,
|
||||
message: '必填项不能为空',
|
||||
trigger: ['blur', 'change']
|
||||
}
|
||||
],
|
||||
pagePath: [
|
||||
{
|
||||
required: true,
|
||||
message: '必填项不能为空',
|
||||
trigger: ['blur', 'change']
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
export const useMenuOa = (ref: any) => {
|
||||
if (ref) menuRef.value = ref
|
||||
|
||||
// 添加主菜单
|
||||
const handleAddMenu = () => {
|
||||
menuList.value.push({
|
||||
name: '菜单名称',
|
||||
has_menu: false,
|
||||
type: 'view',
|
||||
url: '',
|
||||
appid: '',
|
||||
pagepath: '',
|
||||
sub_button: []
|
||||
})
|
||||
}
|
||||
|
||||
// 添加子菜单
|
||||
const handleAddSubMenu = (event?: Menu) => {
|
||||
const index = menuIndex.value
|
||||
if (menuList.value[index].sub_button.length >= 5) {
|
||||
feedback.msgError('已添加上限~')
|
||||
return
|
||||
}
|
||||
menuList.value[index].sub_button.push(event)
|
||||
}
|
||||
|
||||
// 编辑子菜单
|
||||
const handleEditSubMenu = (event: Menu, subIndex: number) => {
|
||||
const index = menuIndex.value
|
||||
menuList.value[index].sub_button[subIndex] = event
|
||||
}
|
||||
|
||||
// 删除主菜单
|
||||
const handleDelMenu = (index: number) => {
|
||||
if (index != 0) {
|
||||
menuIndex.value--
|
||||
}
|
||||
menuList.value.splice(index, 1)
|
||||
}
|
||||
|
||||
// 删除子菜单
|
||||
const handleDelSubMenu = (index: number, subIndex: number) => {
|
||||
menuList.value[index].sub_button.splice(subIndex, 1)
|
||||
}
|
||||
|
||||
// 获取菜单
|
||||
const getOaMenuFunc = async () => {
|
||||
try {
|
||||
menuList.value = await getOaMenu()
|
||||
} catch (error) {
|
||||
console.log('获取菜单=>', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存菜单
|
||||
const handleSave = async () => {
|
||||
const refs = menuRef.value.value
|
||||
for (let i = 0; i < refs.length; i++) {
|
||||
try {
|
||||
await refs[i].menuFormRef.validate()
|
||||
} catch (error) {
|
||||
menuIndex.value = i
|
||||
return
|
||||
}
|
||||
}
|
||||
await setOaMenuSave(menuList.value)
|
||||
}
|
||||
|
||||
// 保存菜单
|
||||
const handlePublish = async () => {
|
||||
const refs = menuRef.value.value
|
||||
for (let i = 0; i < refs.length; i++) {
|
||||
try {
|
||||
await refs[i].menuFormRef.validate()
|
||||
} catch (error) {
|
||||
menuIndex.value = i
|
||||
return
|
||||
}
|
||||
}
|
||||
await setOaMenuPublish(menuList.value)
|
||||
}
|
||||
|
||||
return {
|
||||
menuList,
|
||||
menuIndex,
|
||||
handleAddMenu,
|
||||
handleAddSubMenu,
|
||||
handleEditSubMenu,
|
||||
handleDelMenu,
|
||||
handleDelSubMenu,
|
||||
getOaMenuFunc,
|
||||
handleSave,
|
||||
handlePublish
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-alert
|
||||
type="warning"
|
||||
title="温馨提示:1.粉丝在公众号发送内容时,系统无法匹配情况下发送启用的默认文本回复;2.同时只能启用一个默认回复。"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
</el-card>
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<div>
|
||||
<el-button class="mb-4" type="primary" @click="handleAdd()">
|
||||
<template #icon>
|
||||
<icon name="el-icon-Plus" />
|
||||
</template>
|
||||
新增
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table size="large" :data="pager.lists" v-loading="pager.loading">
|
||||
<el-table-column label="规则名称" prop="name" min-width="120" />
|
||||
<el-table-column label="回复类型" min-width="120">
|
||||
<template #default="{ row }">
|
||||
{{ getContentType(row.content_type) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="回复内容" prop="content" min-width="120" />
|
||||
<el-table-column label="状态" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
v-model="row.status"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
@change="changeStatus(row.id)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="排序" prop="sort" min-width="120" />
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleEdit(row)"> 编辑 </el-button>
|
||||
<el-button type="danger" link @click="handleDelete(row.id)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="flex justify-end mt-4">
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</div>
|
||||
</el-card>
|
||||
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { changeOaReplyStatus, getOaReplyList, oaReplyDel } from '@/api/channel/wx_oa'
|
||||
import { usePaging } from '@/hooks/usePaging'
|
||||
import feedback from '@/utils/feedback'
|
||||
|
||||
import EditPopup from './edit.vue'
|
||||
|
||||
const editRef = shallowRef<InstanceType<typeof EditPopup>>()
|
||||
const showEdit = ref(false)
|
||||
|
||||
const getContentType = computed(() => {
|
||||
return (val: number) => {
|
||||
switch (val) {
|
||||
case 1:
|
||||
return '文本'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const { pager, getLists } = usePaging({
|
||||
fetchFun: getOaReplyList,
|
||||
params: {
|
||||
reply_type: 3
|
||||
}
|
||||
})
|
||||
|
||||
const handleAdd = async () => {
|
||||
showEdit.value = true
|
||||
await nextTick()
|
||||
editRef.value?.open('add', 3)
|
||||
}
|
||||
|
||||
const handleEdit = async (data: any) => {
|
||||
showEdit.value = true
|
||||
await nextTick()
|
||||
editRef.value?.open('edit', 3)
|
||||
editRef.value?.getDetail(data)
|
||||
}
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
await feedback.confirm('确定要删除?')
|
||||
await oaReplyDel({ id })
|
||||
feedback.msgSuccess('删除成功')
|
||||
getLists()
|
||||
}
|
||||
|
||||
const changeStatus = async (id: number) => {
|
||||
try {
|
||||
await changeOaReplyStatus({ id })
|
||||
getLists()
|
||||
} catch (error) {
|
||||
getLists()
|
||||
}
|
||||
}
|
||||
|
||||
getLists()
|
||||
</script>
|
||||
@@ -0,0 +1,190 @@
|
||||
<template>
|
||||
<div class="edit-popup">
|
||||
<popup
|
||||
ref="popupRef"
|
||||
:title="popupTitle"
|
||||
:async="true"
|
||||
width="500px"
|
||||
@confirm="handleSubmit"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
label-width="84px"
|
||||
:rules="formRules"
|
||||
class="pr-10"
|
||||
>
|
||||
<el-form-item label="规则名称" prop="name">
|
||||
<div class="flex-1">
|
||||
<el-input v-model="formData.name" placeholder="请输入规则名称" />
|
||||
<div class="form-tips">方便通过名称管理关注回复内容</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="关键词" prop="keyword" v-if="formData.reply_type == 2">
|
||||
<div class="flex-1">
|
||||
<el-input v-model="formData.keyword" placeholder="请输入关键词" />
|
||||
<div class="form-tips">方便通过名称管理关注回复内容</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="匹配方式" prop="matching_type" v-if="formData.reply_type == 2">
|
||||
<div class="flex-1">
|
||||
<el-radio-group v-model="formData.matching_type">
|
||||
<el-radio :value="1">全匹配</el-radio>
|
||||
<el-radio :value="2">模糊匹配</el-radio>
|
||||
</el-radio-group>
|
||||
<div class="form-tips">模糊匹配时,关键词部分匹配用户输入的内容即可</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="回复类型" prop="content_type" :min="0">
|
||||
<div class="flex-1">
|
||||
<el-radio-group v-model="formData.content_type">
|
||||
<el-radio :value="1">文本</el-radio>
|
||||
</el-radio-group>
|
||||
<div class="form-tips">暂时只支持文本类型</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="回复内容" prop="content">
|
||||
<div class="flex-1">
|
||||
<el-input
|
||||
v-model="formData.content"
|
||||
:autosize="{ minRows: 4, maxRows: 4 }"
|
||||
type="textarea"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
placeholder="请输入回复内容"
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<div class="flex-1">
|
||||
<el-input-number v-model="formData.sort" :min="0" :max="9999" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
label="回复数量"
|
||||
prop="reply_num"
|
||||
required
|
||||
v-if="formData.reply_type == 2"
|
||||
>
|
||||
<div class="flex-1">
|
||||
<el-radio-group v-model="formData.reply_num">
|
||||
<el-radio :value="1">回复匹配首词条</el-radio>
|
||||
</el-radio-group>
|
||||
<div class="form-tips">
|
||||
设置关键词匹配多条时回复的数量,暂时支持回复一条内容
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="启用状态">
|
||||
<el-switch v-model="formData.status" :active-value="1" :inactive-value="0" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</popup>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import type { FormRules } from 'element-plus'
|
||||
|
||||
import { getOaReplyDetail, oaReplyAdd, oaReplyEdit } from '@/api/channel/wx_oa'
|
||||
import Popup from '@/components/popup/index.vue'
|
||||
|
||||
const emit = defineEmits(['success', 'close'])
|
||||
|
||||
const formRef = shallowRef<FormInstance>()
|
||||
const popupRef = shallowRef<InstanceType<typeof Popup>>()
|
||||
const mode = ref('add')
|
||||
const popupTitle = computed(() => {
|
||||
return mode.value == 'edit' ? '编辑' : '新增'
|
||||
})
|
||||
const formData = reactive({
|
||||
id: '',
|
||||
name: '',
|
||||
reply_type: 0,
|
||||
content_type: 1,
|
||||
keyword: '',
|
||||
content: '',
|
||||
matching_type: 1,
|
||||
status: 1,
|
||||
sort: 0,
|
||||
reply_num: 1
|
||||
})
|
||||
|
||||
const formRules: FormRules = {
|
||||
name: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入规则名称',
|
||||
trigger: ['blur']
|
||||
}
|
||||
],
|
||||
keyword: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入关键词',
|
||||
trigger: ['blur']
|
||||
}
|
||||
],
|
||||
matching_type: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择匹配方式',
|
||||
trigger: ['blur']
|
||||
}
|
||||
],
|
||||
content_type: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择回复类型',
|
||||
trigger: ['blur']
|
||||
}
|
||||
],
|
||||
content: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入回复内容',
|
||||
trigger: ['blur']
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
await formRef.value?.validate()
|
||||
mode.value == 'edit' ? await oaReplyEdit(formData) : await oaReplyAdd(formData)
|
||||
popupRef.value?.close()
|
||||
emit('success')
|
||||
}
|
||||
|
||||
const open = (modes = 'add', type = 0) => {
|
||||
mode.value = modes
|
||||
formData.reply_type = type
|
||||
popupRef.value?.open()
|
||||
}
|
||||
|
||||
const setFormData = (data: Record<any, any>) => {
|
||||
for (const key in formData) {
|
||||
if (data[key] != null && data[key] != undefined) {
|
||||
//@ts-ignore
|
||||
formData[key] = data[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getDetail = async (row: Record<string, any>) => {
|
||||
const data = await getOaReplyDetail({
|
||||
id: row.id
|
||||
})
|
||||
setFormData(data)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
open,
|
||||
setFormData,
|
||||
getDetail
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-alert
|
||||
type="warning"
|
||||
title="温馨提示:1.粉丝关注公众号时,会自动发送启用的关注回复;2.同时只能启用一个关注回复。"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
</el-card>
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<div>
|
||||
<el-button class="mb-4" type="primary" @click="handleAdd()">
|
||||
<template #icon>
|
||||
<icon name="el-icon-Plus" />
|
||||
</template>
|
||||
新增
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table size="large" :data="pager.lists" v-loading="pager.loading">
|
||||
<el-table-column label="规则名称" prop="name" min-width="120" />
|
||||
<el-table-column label="回复类型" min-width="120">
|
||||
<template #default="{ row }">
|
||||
{{ getContentType(row.content_type) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="回复内容" prop="content" min-width="120" />
|
||||
<el-table-column label="状态" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
v-model="row.status"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
@change="changeStatus(row.id)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="排序" prop="sort" min-width="120" />
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleEdit(row)"> 编辑 </el-button>
|
||||
<el-button type="danger" link @click="handleDelete(row.id)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="flex justify-end mt-4">
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</div>
|
||||
</el-card>
|
||||
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { changeOaReplyStatus, getOaReplyList, oaReplyDel } from '@/api/channel/wx_oa'
|
||||
import { usePaging } from '@/hooks/usePaging'
|
||||
import feedback from '@/utils/feedback'
|
||||
|
||||
import EditPopup from './edit.vue'
|
||||
|
||||
const editRef = shallowRef<InstanceType<typeof EditPopup>>()
|
||||
const showEdit = ref(false)
|
||||
|
||||
const getContentType = computed(() => {
|
||||
return (val: number) => {
|
||||
switch (val) {
|
||||
case 1:
|
||||
return '文本'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const { pager, getLists } = usePaging({
|
||||
fetchFun: getOaReplyList,
|
||||
params: {
|
||||
reply_type: 1
|
||||
}
|
||||
})
|
||||
|
||||
const handleAdd = async () => {
|
||||
showEdit.value = true
|
||||
await nextTick()
|
||||
editRef.value?.open('add', 1)
|
||||
}
|
||||
|
||||
const handleEdit = async (data: any) => {
|
||||
showEdit.value = true
|
||||
await nextTick()
|
||||
editRef.value?.open('edit', 1)
|
||||
editRef.value?.getDetail(data)
|
||||
}
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
await feedback.confirm('确定要删除?')
|
||||
await oaReplyDel({ id })
|
||||
getLists()
|
||||
}
|
||||
|
||||
const changeStatus = async (id: number) => {
|
||||
try {
|
||||
await changeOaReplyStatus({ id })
|
||||
getLists()
|
||||
} catch (error) {
|
||||
getLists()
|
||||
}
|
||||
}
|
||||
|
||||
getLists()
|
||||
</script>
|
||||
@@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-alert
|
||||
type="warning"
|
||||
title="温馨提示:1.粉丝在公众号发送内容时,通过关键词可触发关键词回复;2.同时可启用多个关键词回复,有多条关键词匹配时优选选择排序靠前的一条"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
</el-card>
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<div>
|
||||
<el-button class="mb-4" type="primary" @click="handleAdd()">
|
||||
<template #icon>
|
||||
<icon name="el-icon-Plus" />
|
||||
</template>
|
||||
新增
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table size="large" :data="pager.lists" v-loading="pager.loading">
|
||||
<el-table-column label="规则名称" prop="name" min-width="120" />
|
||||
|
||||
<el-table-column label="关键词" prop="keyword" min-width="120" />
|
||||
<el-table-column label="匹配方式" min-width="120">
|
||||
<template #default="{ row }">
|
||||
{{ getMatchingType(row.matching_type) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="回复类型" min-width="120">
|
||||
<template #default="{ row }">
|
||||
{{ getContentType(row.content_type) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
v-model="row.status"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
@change="changeStatus(row.id)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="排序" prop="sort" min-width="120" />
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleEdit(row)"> 编辑 </el-button>
|
||||
<el-button type="danger" link @click="handleDelete(row.id)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="flex justify-end mt-4">
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</div>
|
||||
</el-card>
|
||||
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { changeOaReplyStatus, getOaReplyList, oaReplyDel } from '@/api/channel/wx_oa'
|
||||
import { usePaging } from '@/hooks/usePaging'
|
||||
import feedback from '@/utils/feedback'
|
||||
|
||||
import EditPopup from './edit.vue'
|
||||
|
||||
const editRef = shallowRef<InstanceType<typeof EditPopup>>()
|
||||
const showEdit = ref(false)
|
||||
|
||||
const getMatchingType = computed(() => {
|
||||
return (val: number) => {
|
||||
switch (val) {
|
||||
case 1:
|
||||
return '全匹配'
|
||||
case 2:
|
||||
return '模糊匹配'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const getContentType = computed(() => {
|
||||
return (val: number) => {
|
||||
switch (val) {
|
||||
case 1:
|
||||
return '文本'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const { pager, getLists } = usePaging({
|
||||
fetchFun: getOaReplyList,
|
||||
params: {
|
||||
reply_type: 2
|
||||
}
|
||||
})
|
||||
const handleAdd = async () => {
|
||||
showEdit.value = true
|
||||
await nextTick()
|
||||
editRef.value?.open('add', 2)
|
||||
}
|
||||
|
||||
const handleEdit = async (data: any) => {
|
||||
showEdit.value = true
|
||||
await nextTick()
|
||||
editRef.value?.open('edit', 2)
|
||||
editRef.value?.getDetail(data)
|
||||
}
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
await feedback.confirm('确定要删除?')
|
||||
await oaReplyDel({ id })
|
||||
getLists()
|
||||
}
|
||||
|
||||
const changeStatus = async (id: number) => {
|
||||
try {
|
||||
await changeOaReplyStatus({ id })
|
||||
getLists()
|
||||
} catch (error) {
|
||||
getLists()
|
||||
}
|
||||
}
|
||||
|
||||
getLists()
|
||||
</script>
|
||||
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<div class="community-comment-account">
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-form class="mb-[-16px]" :model="queryParams" :inline="true">
|
||||
<el-form-item class="w-[280px]" label="关键词">
|
||||
<el-input
|
||||
v-model="queryParams.keyword"
|
||||
placeholder="输入账号/昵称/用户ID"
|
||||
clearable
|
||||
@keyup.enter="resetPage"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item class="w-[220px]" label="账号状态">
|
||||
<el-select v-model="queryParams.is_disable" clearable>
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="正常" :value="0" />
|
||||
<el-option label="禁用" :value="1" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="resetPage">查询</el-button>
|
||||
<el-button @click="resetParams">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
|
||||
<el-table-column label="ID" prop="id" min-width="70" />
|
||||
<el-table-column label="评论账号" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.account }}</div>
|
||||
<div class="text-xs text-gray-400">SN: {{ row.sn }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="昵称 / 人设" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.nickname }}</div>
|
||||
<el-tag size="small" class="mt-1">{{ row.persona_label }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="账号状态" min-width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.is_disable ? 'danger' : 'success'">
|
||||
{{ row.is_disable ? '禁用' : '正常' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="资讯评论数" prop="article_comment_count" min-width="100" />
|
||||
<el-table-column label="社区评论数" prop="community_comment_count" min-width="100" />
|
||||
<el-table-column label="总评论数" prop="total_comment_count" min-width="100" />
|
||||
<el-table-column label="创建时间" prop="create_time" min-width="160" />
|
||||
</el-table>
|
||||
<div class="flex justify-end mt-4">
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="communityCommentAccount">
|
||||
import { reactive } from 'vue'
|
||||
import { communityCommentAccountLists } from '@/api/community'
|
||||
import { usePaging } from '@/hooks/usePaging'
|
||||
|
||||
const queryParams = reactive({
|
||||
keyword: '',
|
||||
is_disable: ''
|
||||
})
|
||||
|
||||
const { pager, getLists, resetPage, resetParams } = usePaging({
|
||||
fetchFun: communityCommentAccountLists,
|
||||
params: queryParams
|
||||
})
|
||||
|
||||
getLists()
|
||||
</script>
|
||||
@@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<div class="community-comment">
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" :inline="true">
|
||||
<el-form-item class="w-[220px]" label="目标ID">
|
||||
<el-input
|
||||
v-model="queryParams.target_id"
|
||||
placeholder="输入文章ID/帖子ID"
|
||||
clearable
|
||||
@keyup.enter="resetPage"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item class="w-[220px]" label="评论类型">
|
||||
<el-select v-model="queryParams.comment_type" clearable>
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="资讯评论" value="article" />
|
||||
<el-option label="社区评论" value="post" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item class="w-[220px]" label="用户类型">
|
||||
<el-select v-model="queryParams.is_virtual" clearable>
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="虚拟评论" :value="1" />
|
||||
<el-option label="真实评论" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item class="w-[220px]" label="评论状态">
|
||||
<el-select v-model="queryParams.status" clearable>
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="正常" :value="1" />
|
||||
<el-option label="隐藏" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item class="w-[260px]" label="关键词">
|
||||
<el-input
|
||||
v-model="queryParams.keyword"
|
||||
placeholder="输入评论内容/标题/用户"
|
||||
clearable
|
||||
@keyup.enter="resetPage"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="resetPage">查询</el-button>
|
||||
<el-button @click="resetParams">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
|
||||
<el-table-column label="ID" prop="id" min-width="70" />
|
||||
<el-table-column label="评论类型" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.comment_type === 'article' ? 'primary' : 'success'">
|
||||
{{ row.source_type_desc }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="目标信息" min-width="280">
|
||||
<template #default="{ row }">
|
||||
<div class="text-xs text-gray-400">#{{ row.target_id }} · {{ row.target_owner_nickname }}</div>
|
||||
<div class="truncate">{{ row.target_title }}</div>
|
||||
<div class="truncate text-xs text-gray-400">{{ row.target_summary || '-' }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="评论用户" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.nickname }} (ID:{{ row.user_id }})</div>
|
||||
<div class="text-xs text-gray-400">{{ row.account }}</div>
|
||||
<el-tag size="small" class="mt-1" :type="row.is_virtual ? 'warning' : 'info'">
|
||||
{{ row.user_type_desc }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="评论内容" prop="content" min-width="220" show-tooltip-when-overflow />
|
||||
<el-table-column label="点赞" prop="like_count" min-width="70" />
|
||||
<el-table-column label="状态" min-width="90">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
v-perms="['community.communityComment/updateStatus']"
|
||||
v-model="row.status"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
@change="handleStatus($event, row.id, row.comment_type)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="评论时间" prop="create_time" min-width="160" />
|
||||
<el-table-column label="操作" width="100" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-perms="['community.communityComment/delete']"
|
||||
type="danger"
|
||||
link
|
||||
@click="handleDelete(row.id, row.comment_type)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="flex justify-end mt-4">
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="communityComment">
|
||||
import { reactive } from 'vue'
|
||||
import {
|
||||
communityCommentDelete,
|
||||
communityCommentLists,
|
||||
communityCommentStatus
|
||||
} from '@/api/community'
|
||||
import { usePaging } from '@/hooks/usePaging'
|
||||
import feedback from '@/utils/feedback'
|
||||
|
||||
const queryParams = reactive({
|
||||
target_id: '',
|
||||
comment_type: '',
|
||||
is_virtual: '',
|
||||
status: '',
|
||||
keyword: ''
|
||||
})
|
||||
|
||||
const { pager, getLists, resetPage, resetParams } = usePaging({
|
||||
fetchFun: communityCommentLists,
|
||||
params: queryParams
|
||||
})
|
||||
|
||||
const handleStatus = async (status: number, id: number, comment_type: string) => {
|
||||
try {
|
||||
await communityCommentStatus({ id, status, comment_type })
|
||||
getLists()
|
||||
} catch (error) {
|
||||
getLists()
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (id: number, comment_type: string) => {
|
||||
await feedback.confirm('确定要删除该评论?')
|
||||
await communityCommentDelete({ id, comment_type })
|
||||
getLists()
|
||||
}
|
||||
|
||||
getLists()
|
||||
</script>
|
||||
@@ -0,0 +1,210 @@
|
||||
<template>
|
||||
<div class="community-post">
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" :inline="true">
|
||||
<el-form-item class="w-[280px]" label="帖子状态">
|
||||
<el-select v-model="queryParams.status" clearable>
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="待审核" :value="0" />
|
||||
<el-option label="已通过" :value="1" />
|
||||
<el-option label="已拒绝" :value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item class="w-[280px]" label="帖子类型">
|
||||
<el-select v-model="queryParams.post_type" clearable>
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="普通帖子" :value="0" />
|
||||
<el-option label="赛事分析" :value="1" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="resetPage">查询</el-button>
|
||||
<el-button @click="resetParams">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
|
||||
<el-table-column label="ID" prop="id" min-width="60" />
|
||||
<el-table-column label="用户" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.nickname }} (ID:{{ row.user_id }})</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="内容" min-width="200" show-tooltip-when-overflow>
|
||||
<template #default="{ row }">
|
||||
{{ row.content ? row.content.substring(0, 50) : '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" min-width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.post_type === 1" type="warning">赛事分析</el-tag>
|
||||
<el-tag v-else>普通</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="付费" min-width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.is_paid" type="danger">{{ row.price_points }}积分</el-tag>
|
||||
<el-tag v-else type="info">免费</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="浏览" prop="view_count" min-width="70" />
|
||||
<el-table-column label="点赞" prop="like_count" min-width="70" />
|
||||
<el-table-column label="评论" prop="comment_count" min-width="70" />
|
||||
<el-table-column label="置顶" min-width="70">
|
||||
<template #default="{ row }">
|
||||
<el-switch v-perms="['community.communityPost/setTop']" v-model="row.is_top" :active-value="1"
|
||||
:inactive-value="0" @change="handleSetTop($event, row.id)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="热门" min-width="70">
|
||||
<template #default="{ row }">
|
||||
<el-switch v-perms="['community.communityPost/setHot']" v-model="row.is_hot" :active-value="1"
|
||||
:inactive-value="0" @change="handleSetHot($event, row.id)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" min-width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.status === 0" type="warning">待审核</el-tag>
|
||||
<el-tag v-else-if="row.status === 1" type="success">已通过</el-tag>
|
||||
<el-tag v-else type="danger">已拒绝</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="发布时间" prop="create_time" min-width="150" />
|
||||
<el-table-column label="操作" width="230" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button v-perms="['community.communityPost/detail']" type="primary" link
|
||||
@click="handleDetail(row.id)">
|
||||
详情
|
||||
</el-button>
|
||||
<el-button v-if="row.status === 0" v-perms="['community.communityPost/updateStatus']"
|
||||
type="success" link @click="handleAudit(row.id, 1)">
|
||||
通过
|
||||
</el-button>
|
||||
<el-button v-if="row.status === 0" v-perms="['community.communityPost/updateStatus']"
|
||||
type="warning" link @click="handleAudit(row.id, 2)">
|
||||
拒绝
|
||||
</el-button>
|
||||
<el-button v-perms="['community.communityPost/delete']" type="danger" link
|
||||
@click="handleDelete(row.id)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="flex justify-end mt-4">
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</div>
|
||||
</el-card>
|
||||
<el-dialog v-model="showDetail" title="帖子详情" width="700px">
|
||||
<div v-loading="detailLoading">
|
||||
<div class="flex items-center mb-4">
|
||||
<el-avatar :src="detailData.avatar" :size="40" />
|
||||
<div class="ml-3">
|
||||
<div class="font-bold">{{ detailData.nickname }}</div>
|
||||
<div class="text-xs text-gray-400">用户ID: {{ detailData.user_id }} | {{ detailData.create_time }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-auto">
|
||||
<el-tag v-if="detailData.status === 0" type="warning">待审核</el-tag>
|
||||
<el-tag v-else-if="detailData.status === 1" type="success">已通过</el-tag>
|
||||
<el-tag v-else type="danger">已拒绝</el-tag>
|
||||
<el-tag v-if="detailData.post_type === 1" type="warning" class="ml-2">赛事分析</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<el-divider />
|
||||
<div class="mb-4 whitespace-pre-wrap">{{ detailData.content }}</div>
|
||||
<div v-if="detailData.images && detailData.images.length" class="flex flex-wrap gap-2 mb-4">
|
||||
<el-image v-for="(img, idx) in detailData.images" :key="idx" :src="img"
|
||||
:preview-src-list="detailData.images" :initial-index="idx" fit="cover"
|
||||
class="w-[100px] h-[100px] rounded" />
|
||||
</div>
|
||||
<el-divider />
|
||||
<el-descriptions :column="3" border size="small">
|
||||
<el-descriptions-item label="浏览">{{ detailData.view_count || 0 }}</el-descriptions-item>
|
||||
<el-descriptions-item label="点赞">{{ detailData.like_count || 0 }}</el-descriptions-item>
|
||||
<el-descriptions-item label="评论">{{ detailData.comment_count || 0 }}</el-descriptions-item>
|
||||
<el-descriptions-item label="分享">{{ detailData.share_count || 0 }}</el-descriptions-item>
|
||||
<el-descriptions-item label="付费解锁">{{ detailData.paid_count || 0 }}人</el-descriptions-item>
|
||||
<el-descriptions-item label="付费积分">{{ detailData.is_paid ? detailData.price_points + '积分' : '免费'
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="置顶">{{ detailData.is_top ? '是' : '否' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="热门">{{ detailData.is_hot ? '是' : '否' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">{{ detailData.update_time || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="showDetail = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup name="communityPost">
|
||||
import {
|
||||
communityPostLists,
|
||||
communityPostDetail,
|
||||
communityPostStatus,
|
||||
communityPostSetTop,
|
||||
communityPostSetHot,
|
||||
communityPostDelete
|
||||
} from '@/api/community'
|
||||
import { usePaging } from '@/hooks/usePaging'
|
||||
import feedback from '@/utils/feedback'
|
||||
|
||||
const queryParams = reactive({
|
||||
status: '',
|
||||
post_type: ''
|
||||
})
|
||||
|
||||
const { pager, getLists, resetPage, resetParams } = usePaging({
|
||||
fetchFun: communityPostLists,
|
||||
params: queryParams
|
||||
})
|
||||
|
||||
const handleAudit = async (id: number, status: number) => {
|
||||
await feedback.confirm(status === 1 ? '确定通过该帖子?' : '确定拒绝该帖子?')
|
||||
await communityPostStatus({ id, status })
|
||||
getLists()
|
||||
}
|
||||
|
||||
const handleSetTop = async (is_top: any, id: number) => {
|
||||
try {
|
||||
await communityPostSetTop({ id, is_top })
|
||||
getLists()
|
||||
} catch (error) {
|
||||
getLists()
|
||||
}
|
||||
}
|
||||
|
||||
const handleSetHot = async (is_hot: any, id: number) => {
|
||||
try {
|
||||
await communityPostSetHot({ id, is_hot })
|
||||
getLists()
|
||||
} catch (error) {
|
||||
getLists()
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
await feedback.confirm('确定要删除该帖子?')
|
||||
await communityPostDelete({ id })
|
||||
getLists()
|
||||
}
|
||||
|
||||
const showDetail = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
const detailData = ref<any>({})
|
||||
|
||||
const handleDetail = async (id: number) => {
|
||||
showDetail.value = true
|
||||
detailLoading.value = true
|
||||
try {
|
||||
const res = await communityPostDetail({ id })
|
||||
detailData.value = res
|
||||
} finally {
|
||||
detailLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
getLists()
|
||||
</script>
|
||||
@@ -0,0 +1,171 @@
|
||||
<template>
|
||||
<div class="community-tag">
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" :inline="true">
|
||||
<el-form-item class="w-[280px]" label="标签名称">
|
||||
<el-input
|
||||
v-model="queryParams.name"
|
||||
placeholder="输入标签名称"
|
||||
clearable
|
||||
@keyup.enter="resetPage"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="resetPage">查询</el-button>
|
||||
<el-button @click="resetParams">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<div>
|
||||
<el-button
|
||||
v-perms="['community.communityTag/add']"
|
||||
type="primary"
|
||||
class="mb-4"
|
||||
@click="handleAdd"
|
||||
>
|
||||
<template #icon>
|
||||
<icon name="el-icon-Plus" />
|
||||
</template>
|
||||
新增标签
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
|
||||
<el-table-column label="ID" prop="id" min-width="60" />
|
||||
<el-table-column label="标签名称" prop="name" min-width="120" />
|
||||
<el-table-column label="帖子数" prop="post_count" min-width="80" />
|
||||
<el-table-column label="排序" prop="sort" min-width="80" />
|
||||
<el-table-column label="热门" min-width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.is_hot" type="danger">热门</el-tag>
|
||||
<el-tag v-else type="info">普通</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" min-width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.status === 1" type="success">启用</el-tag>
|
||||
<el-tag v-else type="danger">禁用</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-perms="['community.communityTag/edit']"
|
||||
type="primary"
|
||||
link
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-perms="['community.communityTag/delete']"
|
||||
type="danger"
|
||||
link
|
||||
@click="handleDelete(row.id)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="flex justify-end mt-4">
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 新增/编辑弹窗 -->
|
||||
<el-dialog
|
||||
v-model="showEdit"
|
||||
:title="editData.id ? '编辑标签' : '新增标签'"
|
||||
width="500px"
|
||||
>
|
||||
<el-form ref="editFormRef" :model="editData" :rules="editRules" label-width="80px">
|
||||
<el-form-item label="标签名称" prop="name">
|
||||
<el-input v-model="editData.name" placeholder="请输入标签名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="editData.sort" :min="0" :max="9999" />
|
||||
</el-form-item>
|
||||
<el-form-item label="热门">
|
||||
<el-switch v-model="editData.is_hot" :active-value="1" :inactive-value="0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-switch v-model="editData.status" :active-value="1" :inactive-value="0" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showEdit = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup name="communityTag">
|
||||
import {
|
||||
communityTagLists,
|
||||
communityTagAdd,
|
||||
communityTagEdit,
|
||||
communityTagDelete
|
||||
} from '@/api/community'
|
||||
import { usePaging } from '@/hooks/usePaging'
|
||||
import feedback from '@/utils/feedback'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
|
||||
const queryParams = reactive({
|
||||
name: ''
|
||||
})
|
||||
|
||||
const { pager, getLists, resetPage, resetParams } = usePaging({
|
||||
fetchFun: communityTagLists,
|
||||
params: queryParams
|
||||
})
|
||||
|
||||
const showEdit = ref(false)
|
||||
const editFormRef = ref<FormInstance>()
|
||||
const editData = reactive({
|
||||
id: 0,
|
||||
name: '',
|
||||
sort: 0,
|
||||
is_hot: 0,
|
||||
status: 1
|
||||
})
|
||||
|
||||
const editRules = {
|
||||
name: [{ required: true, message: '请输入标签名称', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
Object.assign(editData, { id: 0, name: '', sort: 0, is_hot: 0, status: 1 })
|
||||
showEdit.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (row: any) => {
|
||||
Object.assign(editData, {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
sort: row.sort,
|
||||
is_hot: row.is_hot,
|
||||
status: row.status
|
||||
})
|
||||
showEdit.value = true
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
await editFormRef.value?.validate()
|
||||
if (editData.id) {
|
||||
await communityTagEdit(editData)
|
||||
} else {
|
||||
await communityTagAdd(editData)
|
||||
}
|
||||
showEdit.value = false
|
||||
getLists()
|
||||
}
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
await feedback.confirm('确定要删除该标签?')
|
||||
await communityTagDelete({ id })
|
||||
getLists()
|
||||
}
|
||||
|
||||
getLists()
|
||||
</script>
|
||||
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<popup ref="popupRef" :title="popupTitle" width="500px" @confirm="handleConfirm" :async="true" @close="popupClose">
|
||||
<div class="pr-8">
|
||||
<el-form ref="formRef" :model="formData" label-width="120px" :rules="formRules">
|
||||
<el-form-item label="调整账户">
|
||||
<el-radio-group v-model="formData.type">
|
||||
<el-radio value="money">余额</el-radio>
|
||||
<el-radio value="points">积分</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="'当前' + typeLabel">
|
||||
{{ currentValue }} {{ unit }}
|
||||
</el-form-item>
|
||||
<el-form-item :label="typeLabel + '增减'" required prop="action">
|
||||
<el-radio-group v-model="formData.action">
|
||||
<el-radio :value="1">增加{{ typeLabel }}</el-radio>
|
||||
<el-radio :value="2">扣减{{ typeLabel }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="'调整' + typeLabel" prop="num">
|
||||
<el-input :model-value="formData.num" :placeholder="'请输入调整的' + typeLabel" type="number"
|
||||
@input="numberValidate" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="'调整后' + typeLabel">
|
||||
{{ adjustedValue }} {{ unit }}
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="4" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</popup>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
import Popup from '@/components/popup/index.vue'
|
||||
import feedback from '@/utils/feedback'
|
||||
|
||||
const formRef = shallowRef<FormInstance>()
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
userMoney: {
|
||||
type: [Number, String],
|
||||
default: 0
|
||||
},
|
||||
userPoints: {
|
||||
type: [Number, String],
|
||||
default: 0
|
||||
},
|
||||
defaultType: {
|
||||
type: String,
|
||||
default: 'money'
|
||||
}
|
||||
})
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:show', value: boolean): void
|
||||
(event: 'confirm', value: any): void
|
||||
}>()
|
||||
const formData = reactive({
|
||||
type: 'money',
|
||||
action: 1,
|
||||
num: '',
|
||||
remark: ''
|
||||
})
|
||||
const popupRef = shallowRef<InstanceType<typeof Popup>>()
|
||||
|
||||
const typeLabel = computed(() => formData.type === 'money' ? '余额' : '积分')
|
||||
const unit = computed(() => formData.type === 'money' ? '元' : '分')
|
||||
const popupTitle = computed(() => typeLabel.value + '调整')
|
||||
|
||||
const currentValue = computed(() => {
|
||||
return formData.type === 'money' ? Number(props.userMoney) : Number(props.userPoints)
|
||||
})
|
||||
|
||||
const adjustedValue = computed(() => {
|
||||
return currentValue.value + Number(formData.num) * (formData.action == 1 ? 1 : -1)
|
||||
})
|
||||
|
||||
const formRules: FormRules = {
|
||||
num: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入调整数量'
|
||||
}
|
||||
]
|
||||
}
|
||||
const numberValidate = (value: string) => {
|
||||
if (value.includes('-')) {
|
||||
return feedback.msgError('请输入正整数')
|
||||
}
|
||||
formData.num = value
|
||||
}
|
||||
const handleConfirm = async () => {
|
||||
await formRef.value?.validate()
|
||||
emit('confirm', { ...formData })
|
||||
}
|
||||
|
||||
const popupClose = () => {
|
||||
emit('update:show', false)
|
||||
formRef.value?.resetFields()
|
||||
formData.type = 'money'
|
||||
formData.action = 1
|
||||
formData.num = ''
|
||||
formData.remark = ''
|
||||
}
|
||||
watch(
|
||||
() => props.show,
|
||||
(val) => {
|
||||
if (val) {
|
||||
formData.type = props.defaultType
|
||||
popupRef.value?.open()
|
||||
} else {
|
||||
popupRef.value?.close()
|
||||
}
|
||||
}
|
||||
)
|
||||
watch(adjustedValue, (val) => {
|
||||
if (val < 0) {
|
||||
feedback.msgError('调整后' + typeLabel.value + '不能小于0')
|
||||
formData.num = ''
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-page-header content="用户详情" @back="$router.back()" />
|
||||
</el-card>
|
||||
<el-card class="mt-4 !border-none" header="基本资料" shadow="never">
|
||||
<el-form ref="formRef" class="ls-form" :model="formData" label-width="120px">
|
||||
<div class="bg-page flex py-5 mb-10 items-center">
|
||||
<div class="basis-40 flex flex-col justify-center items-center">
|
||||
<div class="mb-2 text-tx-regular">用户头像</div>
|
||||
<el-avatar :src="formData.avatar" :size="58" />
|
||||
<material-picker ref="avatarPickerRef" v-model="formData.avatar" :limit="1" size="58px"
|
||||
hidden-upload @change="handleAvatarChange" />
|
||||
<el-button class="mt-2" type="primary" link v-perms="['user.user/edit']"
|
||||
@click="handleAvatarPickerOpen">
|
||||
修改头像
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="basis-40 flex flex-col justify-center items-center">
|
||||
<div class="text-tx-regular">账户余额</div>
|
||||
<div class="mt-2 flex items-center">
|
||||
¥{{ formData.user_money }}
|
||||
<el-button v-perms="['user.user/adjustMoney']" type="primary" link
|
||||
@click="handleAdjust(formData.user_money)">
|
||||
调整
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-form-item label="用户昵称:">
|
||||
{{ formData.nickname }}
|
||||
</el-form-item>
|
||||
<el-form-item label="账号:">
|
||||
{{ formData.account }}
|
||||
<popover-input class="ml-[10px]" @confirm="handleEdit($event, 'account')" :limit="32"
|
||||
v-perms="['user.user/edit']">
|
||||
<el-button type="primary" link>
|
||||
<icon name="el-icon-EditPen" />
|
||||
</el-button>
|
||||
</popover-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="真实姓名:">
|
||||
{{ formData.real_name || '-' }}
|
||||
<popover-input class="ml-[10px]" @confirm="handleEdit($event, 'real_name')" :limit="32"
|
||||
v-perms="['user.user/edit']">
|
||||
<el-button type="primary" link>
|
||||
<icon name="el-icon-EditPen" />
|
||||
</el-button>
|
||||
</popover-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="性别:">
|
||||
{{ formData.sex }}
|
||||
<popover-input class="ml-[10px]" type="select" :options="[
|
||||
{
|
||||
label: '未知',
|
||||
value: 0
|
||||
},
|
||||
{
|
||||
label: '男',
|
||||
value: 1
|
||||
},
|
||||
{
|
||||
label: '女',
|
||||
value: 2
|
||||
}
|
||||
]" @confirm="handleEdit($event, 'sex')" v-perms="['user.user/edit']">
|
||||
<el-button type="primary" link>
|
||||
<icon name="el-icon-EditPen" />
|
||||
</el-button>
|
||||
</popover-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="联系电话:">
|
||||
{{ formData.mobile || '-' }}
|
||||
<popover-input class="ml-[10px]" type="number" @confirm="handleEdit($event, 'mobile')"
|
||||
v-perms="['user.user/edit']">
|
||||
<el-button type="primary" link>
|
||||
<icon name="el-icon-EditPen" />
|
||||
</el-button>
|
||||
</popover-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="注册来源:"> {{ formData.channel }} </el-form-item>
|
||||
<el-form-item label="注册时间:"> {{ formData.create_time }} </el-form-item>
|
||||
<el-form-item label="最近登录时间:"> {{ formData.login_time }} </el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<account-adjust v-model:show="adjustState.show" :value="adjustState.value" @confirm="handleConfirmAdjust" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="consumerDetail">
|
||||
import type { FormInstance } from 'element-plus'
|
||||
|
||||
import { adjustMoney, getUserDetail, userEdit } from '@/api/consumer'
|
||||
import { isEmpty } from '@/utils/util'
|
||||
|
||||
import AccountAdjust from '../components/account-adjust.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const formData = reactive({
|
||||
avatar: '',
|
||||
channel: '',
|
||||
create_time: '',
|
||||
login_time: '',
|
||||
mobile: '',
|
||||
nickname: '',
|
||||
real_name: 0,
|
||||
sex: 0,
|
||||
sn: '',
|
||||
account: '',
|
||||
user_money: ''
|
||||
})
|
||||
|
||||
const adjustState = reactive({
|
||||
show: false,
|
||||
value: ''
|
||||
})
|
||||
const formRef = shallowRef<FormInstance>()
|
||||
const avatarPickerRef = shallowRef()
|
||||
|
||||
const getDetails = async () => {
|
||||
const data = await getUserDetail({
|
||||
id: route.query.id
|
||||
})
|
||||
Object.keys(formData).forEach((key) => {
|
||||
//@ts-ignore
|
||||
formData[key] = data[key]
|
||||
})
|
||||
}
|
||||
|
||||
const handleEdit = async (value: string, field: string) => {
|
||||
if (isEmpty(value)) return
|
||||
await userEdit({
|
||||
id: route.query.id,
|
||||
field,
|
||||
value
|
||||
})
|
||||
getDetails()
|
||||
}
|
||||
|
||||
const handleAvatarChange = (value: string) => {
|
||||
handleEdit(value, 'avatar')
|
||||
}
|
||||
|
||||
const handleAvatarPickerOpen = () => {
|
||||
avatarPickerRef.value?.showPopup(-1)
|
||||
}
|
||||
|
||||
const handleAdjust = (value: string) => {
|
||||
adjustState.show = true
|
||||
adjustState.value = value
|
||||
}
|
||||
const handleConfirmAdjust = async (value: any) => {
|
||||
await adjustMoney({ user_id: route.query.id, ...value })
|
||||
adjustState.show = false
|
||||
getDetails()
|
||||
}
|
||||
getDetails()
|
||||
</script>
|
||||
@@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<div>
|
||||
<div class="text-lg font-medium">{{ pageTitle }}</div>
|
||||
<div class="text-sm text-gray-400">{{ pageTips }}</div>
|
||||
</div>
|
||||
<el-tag :type="isSystemList ? 'warning' : 'success'">
|
||||
{{ isSystemList ? '系统账号' : '自注册账号' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" :inline="true">
|
||||
<el-form-item class="w-[280px]" label="用户信息">
|
||||
<el-input v-model="queryParams.keyword" placeholder="账号/昵称/手机号码" clearable
|
||||
@keyup.enter="resetPage" />
|
||||
</el-form-item>
|
||||
<el-form-item label="注册时间">
|
||||
<daterange-picker v-model:startTime="queryParams.create_time_start"
|
||||
v-model:endTime="queryParams.create_time_end" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="!isSystemList" class="w-[280px]" label="注册来源">
|
||||
<el-select v-model="queryParams.channel">
|
||||
<el-option v-for="(item, key) in ClientMap" :key="key" :label="item" :value="key" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="resetPage">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
<export-data class="ml-2.5" :fetch-fun="getUserList" :params="queryParams"
|
||||
:page-size="pager.size" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
|
||||
<el-table-column label="头像" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<el-avatar :src="row.avatar" :size="50" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="账号归属" prop="account_scope_desc" min-width="110">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.is_system_account ? 'warning' : 'success'">
|
||||
{{ row.account_scope_desc }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="isSystemList" label="系统账号类型" prop="account_type_desc" min-width="130">
|
||||
<template #default="{ row }">
|
||||
<el-tag type="info">{{ row.account_type_desc || '系统账号' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="昵称" prop="nickname" min-width="100" />
|
||||
<el-table-column label="账号" prop="account" min-width="120" />
|
||||
<el-table-column label="手机号码" prop="mobile" min-width="100" />
|
||||
<el-table-column label="余额" prop="user_money" min-width="100" />
|
||||
<el-table-column label="积分" prop="user_points" min-width="100" />
|
||||
<el-table-column label="注册来源" prop="channel" min-width="100" />
|
||||
<el-table-column label="注册时间" prop="create_time" min-width="120" />
|
||||
<el-table-column label="操作" width="240" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleAdjust(row, 'money')">调整余额</el-button>
|
||||
<el-button type="primary" link @click="handleAdjust(row, 'points')">调整积分</el-button>
|
||||
<el-button v-perms="['user.user/detail']" type="primary" link>
|
||||
<router-link :to="{
|
||||
path: getRoutePath('user.user/detail'),
|
||||
query: {
|
||||
id: row.id
|
||||
}
|
||||
}">
|
||||
详情
|
||||
</router-link>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="flex justify-end mt-4">
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</div>
|
||||
</el-card>
|
||||
<account-adjust v-model:show="showAdjust" :user-money="adjustRow.user_money"
|
||||
:user-points="adjustRow.user_points" :default-type="adjustType" @confirm="handleAdjustConfirm" />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup name="consumerLists">
|
||||
import { getUserList, adjustAccount } from '@/api/consumer'
|
||||
import { ClientMap } from '@/enums/appEnums'
|
||||
import { usePaging } from '@/hooks/usePaging'
|
||||
import { getRoutePath } from '@/router'
|
||||
import AccountAdjust from '../components/account-adjust.vue'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const queryParams = reactive({
|
||||
keyword: '',
|
||||
channel: '',
|
||||
create_time_start: '',
|
||||
create_time_end: '',
|
||||
account_scope: 'registered'
|
||||
})
|
||||
|
||||
const { pager, getLists, resetPage } = usePaging({
|
||||
fetchFun: getUserList,
|
||||
params: queryParams
|
||||
})
|
||||
|
||||
const routeAccountScope = computed(() => {
|
||||
return route.query.account_scope === 'system' ? 'system' : 'registered'
|
||||
})
|
||||
const isSystemList = computed(() => queryParams.account_scope === 'system')
|
||||
const pageTitle = computed(() => (isSystemList.value ? '系统账号' : '自注册账号'))
|
||||
const pageTips = computed(() =>
|
||||
isSystemList.value
|
||||
? '展示评论账号、特朗普特殊账号、彩票账号等非自注册系统账号'
|
||||
: '展示用户通过 H5、PC、App、小程序等正常注册渠道创建的账号'
|
||||
)
|
||||
|
||||
const syncScopeFromRoute = () => {
|
||||
queryParams.account_scope = routeAccountScope.value
|
||||
}
|
||||
|
||||
const showAdjust = ref(false)
|
||||
const adjustRow = ref<any>({})
|
||||
const adjustType = ref('money')
|
||||
|
||||
const handleAdjust = (row: any, type: string) => {
|
||||
adjustRow.value = row
|
||||
adjustType.value = type
|
||||
showAdjust.value = true
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
queryParams.keyword = ''
|
||||
queryParams.channel = ''
|
||||
queryParams.create_time_start = ''
|
||||
queryParams.create_time_end = ''
|
||||
syncScopeFromRoute()
|
||||
resetPage()
|
||||
}
|
||||
|
||||
const handleAdjustConfirm = async (formData: any) => {
|
||||
await adjustAccount({
|
||||
user_id: adjustRow.value.id,
|
||||
type: formData.type,
|
||||
action: formData.action,
|
||||
num: formData.num,
|
||||
remark: formData.remark
|
||||
})
|
||||
showAdjust.value = false
|
||||
getLists()
|
||||
}
|
||||
|
||||
onActivated(() => {
|
||||
syncScopeFromRoute()
|
||||
getLists()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => route.query.account_scope,
|
||||
() => {
|
||||
syncScopeFromRoute()
|
||||
resetPage()
|
||||
}
|
||||
)
|
||||
|
||||
syncScopeFromRoute()
|
||||
getLists()
|
||||
</script>
|
||||
@@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<div class="crawl-error-log">
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" :inline="true">
|
||||
<el-form-item class="w-[200px]" label="任务名称">
|
||||
<el-input v-model="queryParams.task_name" placeholder="任务名称" clearable @keyup.enter="resetPage" />
|
||||
</el-form-item>
|
||||
<el-form-item class="w-[200px]" label="错误类型">
|
||||
<el-input v-model="queryParams.error_type" placeholder="错误类型" clearable @keyup.enter="resetPage" />
|
||||
</el-form-item>
|
||||
<el-form-item class="w-[160px]" label="通道">
|
||||
<el-select v-model="queryParams.channel" clearable placeholder="全部">
|
||||
<el-option label="api" value="api" />
|
||||
<el-option label="tls" value="tls" />
|
||||
<el-option label="playwright" value="playwright" />
|
||||
<el-option label="page" value="page" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item class="w-[160px]" label="HTTP状态">
|
||||
<el-input v-model="queryParams.http_status" placeholder="如 429" clearable @keyup.enter="resetPage" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="resetPage">查询</el-button>
|
||||
<el-button @click="resetParams">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<div class="flex justify-between mb-4">
|
||||
<span class="text-lg font-medium">爬虫错误日志</span>
|
||||
<el-button type="danger" :disabled="!selectIds.length" @click="handleBatchDelete">
|
||||
批量删除 ({{ selectIds.length }})
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table size="large" v-loading="pager.loading" :data="pager.lists" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column label="ID" prop="id" width="70" />
|
||||
<el-table-column label="任务" prop="task_name" width="130" />
|
||||
<el-table-column label="通道" prop="channel" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="channelTagType(row.channel)">{{ row.channel }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="HTTP" prop="http_status" width="70" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="row.http_status >= 400 ? 'danger' : row.http_status === 0 ? 'info' : 'warning'">
|
||||
{{ row.http_status }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="错误类型" prop="error_type" width="140" />
|
||||
<el-table-column label="请求URL" prop="request_url" min-width="240" show-overflow-tooltip />
|
||||
<el-table-column label="错误信息" prop="error_message" min-width="240" show-overflow-tooltip />
|
||||
<el-table-column label="时间" prop="created_at" width="170" />
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleDetail(row)">详情</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(row.id)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="flex justify-end mt-4">
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<el-dialog v-model="showDetail" title="错误详情" width="700px">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="ID">{{ detailData.id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="任务">{{ detailData.task_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="通道">{{ detailData.channel }}</el-descriptions-item>
|
||||
<el-descriptions-item label="HTTP状态">{{ detailData.http_status }}</el-descriptions-item>
|
||||
<el-descriptions-item label="请求方法">{{ detailData.request_method }}</el-descriptions-item>
|
||||
<el-descriptions-item label="错误类型">{{ detailData.error_type }}</el-descriptions-item>
|
||||
<el-descriptions-item label="时间" :span="2">{{ detailData.created_at }}</el-descriptions-item>
|
||||
<el-descriptions-item label="请求URL" :span="2">
|
||||
<span class="break-all">{{ detailData.request_url }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="错误信息" :span="2">
|
||||
<span class="break-all whitespace-pre-wrap">{{ detailData.error_message }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item v-if="detailData.request_params" label="请求参数" :span="2">
|
||||
<span class="break-all whitespace-pre-wrap text-xs">{{ detailData.request_params }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item v-if="detailData.response_body" label="响应内容" :span="2">
|
||||
<span class="break-all whitespace-pre-wrap text-xs">{{ detailData.response_body }}</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup name="crawlerErrorLog">
|
||||
import { crawlErrorLogLists, crawlErrorLogDetail, crawlErrorLogDelete } from '@/api/crawler'
|
||||
import { usePaging } from '@/hooks/usePaging'
|
||||
import feedback from '@/utils/feedback'
|
||||
|
||||
const queryParams = reactive({
|
||||
task_name: '',
|
||||
error_type: '',
|
||||
channel: '',
|
||||
http_status: ''
|
||||
})
|
||||
|
||||
const { pager, getLists, resetPage, resetParams } = usePaging({
|
||||
fetchFun: crawlErrorLogLists,
|
||||
params: queryParams
|
||||
})
|
||||
|
||||
const channelTagType = (ch: string) => {
|
||||
const map: Record<string, string> = { api: '', tls: 'warning', playwright: 'success', page: 'info' }
|
||||
return map[ch] || 'info'
|
||||
}
|
||||
|
||||
const selectIds = ref<number[]>([])
|
||||
const handleSelectionChange = (rows: any[]) => {
|
||||
selectIds.value = rows.map((r) => r.id)
|
||||
}
|
||||
|
||||
const showDetail = ref(false)
|
||||
const detailData = ref<any>({})
|
||||
const handleDetail = async (row: any) => {
|
||||
const res = await crawlErrorLogDetail({ id: row.id })
|
||||
detailData.value = res
|
||||
showDetail.value = true
|
||||
}
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
await feedback.confirm('确定删除该错误记录?')
|
||||
await crawlErrorLogDelete({ ids: [id] })
|
||||
getLists()
|
||||
}
|
||||
|
||||
const handleBatchDelete = async () => {
|
||||
await feedback.confirm(`确定删除选中的 ${selectIds.value.length} 条记录?`)
|
||||
await crawlErrorLogDelete({ ids: selectIds.value })
|
||||
getLists()
|
||||
}
|
||||
|
||||
getLists()
|
||||
</script>
|
||||
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<div class="crawler-task-log">
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" :inline="true">
|
||||
<el-form-item class="w-[200px]" label="任务名称">
|
||||
<el-input v-model="queryParams.task_name" placeholder="任务名称" clearable @keyup.enter="resetPage" />
|
||||
</el-form-item>
|
||||
<el-form-item class="w-[180px]" label="动作">
|
||||
<el-input v-model="queryParams.action" placeholder="如 nba_match" clearable @keyup.enter="resetPage" />
|
||||
</el-form-item>
|
||||
<el-form-item class="w-[150px]" label="状态">
|
||||
<el-select v-model="queryParams.status" clearable placeholder="全部">
|
||||
<el-option label="执行中" :value="0" />
|
||||
<el-option label="成功" :value="1" />
|
||||
<el-option label="失败" :value="2" />
|
||||
<el-option label="已跳过" :value="3" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item class="w-[150px]" label="来源">
|
||||
<el-input v-model="queryParams.source" placeholder="docker" clearable @keyup.enter="resetPage" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="resetPage">查询</el-button>
|
||||
<el-button @click="resetParams">重置</el-button>
|
||||
<el-button :loading="pager.loading" @click="getLists">刷新</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<div class="flex justify-between mb-4">
|
||||
<span class="text-lg font-medium">爬虫任务日志</span>
|
||||
</div>
|
||||
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
|
||||
<el-table-column label="ID" prop="id" width="80" />
|
||||
<el-table-column label="任务" prop="task_name" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="动作" prop="action" width="150" />
|
||||
<el-table-column label="规则" prop="cron_expression" width="120" />
|
||||
<el-table-column label="状态" prop="status" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="statusTagType(row.status)">
|
||||
{{ row.status_text }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="耗时(s)" prop="elapsed" width="90" />
|
||||
<el-table-column label="容器" prop="container_id" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column label="输出" min-width="260" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ row.output_preview || row.error_message || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="开始时间" prop="started_at_text" width="170" />
|
||||
<el-table-column label="结束时间" prop="finished_at_text" width="170" />
|
||||
<el-table-column label="操作" width="90" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleDetail(row)">详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="flex justify-end mt-4">
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="showDetail" title="任务日志详情" width="820px">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="ID">{{ detailData.id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag size="small" :type="statusTagType(detailData.status)">
|
||||
{{ detailData.status_text }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="任务">{{ detailData.task_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="动作">{{ detailData.action }}</el-descriptions-item>
|
||||
<el-descriptions-item label="规则">{{ detailData.cron_expression }}</el-descriptions-item>
|
||||
<el-descriptions-item label="耗时">{{ detailData.elapsed }}s</el-descriptions-item>
|
||||
<el-descriptions-item label="来源">{{ detailData.source }}</el-descriptions-item>
|
||||
<el-descriptions-item label="容器">{{ detailData.container_id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="开始时间">{{ detailData.started_at_text }}</el-descriptions-item>
|
||||
<el-descriptions-item label="结束时间">{{ detailData.finished_at_text }}</el-descriptions-item>
|
||||
<el-descriptions-item v-if="detailData.error_message" label="错误" :span="2">
|
||||
<span class="break-all whitespace-pre-wrap">{{ detailData.error_message }}</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div class="text-sm text-gray-500 mt-4 mb-2">输出日志:</div>
|
||||
<el-input type="textarea" :model-value="detailData.output || ''" :autosize="{ minRows: 10, maxRows: 28 }" readonly />
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="crawlerTaskLog">
|
||||
import { crawlerTaskLogDetail, crawlerTaskLogLists } from '@/api/crawler'
|
||||
import { usePaging } from '@/hooks/usePaging'
|
||||
|
||||
const queryParams = reactive({
|
||||
task_name: '',
|
||||
action: '',
|
||||
status: '',
|
||||
source: ''
|
||||
})
|
||||
|
||||
const { pager, getLists, resetPage, resetParams } = usePaging({
|
||||
fetchFun: crawlerTaskLogLists,
|
||||
params: queryParams
|
||||
})
|
||||
|
||||
const statusTagType = (status: number) => {
|
||||
const map: Record<number, string> = {
|
||||
0: 'warning',
|
||||
1: 'success',
|
||||
2: 'danger',
|
||||
3: 'info'
|
||||
}
|
||||
return map[Number(status)] || 'info'
|
||||
}
|
||||
|
||||
const showDetail = ref(false)
|
||||
const detailData = ref<any>({})
|
||||
|
||||
const handleDetail = async (row: any) => {
|
||||
detailData.value = await crawlerTaskLogDetail({ id: row.id })
|
||||
showDetail.value = true
|
||||
}
|
||||
|
||||
getLists()
|
||||
</script>
|
||||
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<draggable
|
||||
class="draggable"
|
||||
v-model="navLists"
|
||||
animation="300"
|
||||
handle=".drag-move"
|
||||
item-key="index"
|
||||
>
|
||||
<template v-slot:item="{ element: item, index }">
|
||||
<del-wrap class="w-[467px]" :key="index" @close="handleDelete(index)">
|
||||
<div class="bg-fill-light flex items-center w-full p-4 mb-4">
|
||||
<material-picker
|
||||
v-model="item.image"
|
||||
upload-class="bg-body"
|
||||
size="60px"
|
||||
exclude-domain
|
||||
>
|
||||
<template #upload>
|
||||
<div class="upload-btn w-[60px] h-[60px]">
|
||||
<icon name="el-icon-Plus" :size="20" />
|
||||
</div>
|
||||
</template>
|
||||
</material-picker>
|
||||
<div class="ml-3 flex-1">
|
||||
<div class="flex items-center">
|
||||
<span class="text-tx-regular flex-none mr-3">名称</span>
|
||||
<el-input v-model="item.name" placeholder="请输入名称" />
|
||||
</div>
|
||||
<div class="flex items-center mt-[18px]">
|
||||
<span class="text-tx-regular flex-none mr-3">链接</span>
|
||||
<link-picker v-model="item.link" />
|
||||
</div>
|
||||
<el-form-item label="是否显示" class="mt-[18px]">
|
||||
<div class="flex-1 flex items-center">
|
||||
<el-switch
|
||||
v-model="item.is_show"
|
||||
active-value="1"
|
||||
inactive-value="0"
|
||||
/>
|
||||
<div class="drag-move cursor-move ml-auto">
|
||||
<icon name="el-icon-Rank" size="18" />
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</del-wrap>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
<div>
|
||||
<el-button type="primary" @click="handleAdd">添加</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue'
|
||||
import Draggable from 'vuedraggable'
|
||||
|
||||
import feedback from '@/utils/feedback'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Array as PropType<any[]>,
|
||||
default: () => []
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: 100
|
||||
},
|
||||
min: {
|
||||
type: Number,
|
||||
default: 1
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const navLists = computed({
|
||||
get() {
|
||||
return props.modelValue
|
||||
},
|
||||
set(value) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
|
||||
const handleAdd = () => {
|
||||
if (props.modelValue?.length < props.max) {
|
||||
navLists.value.push({
|
||||
image: '',
|
||||
name: '导航名称',
|
||||
link: {},
|
||||
is_show: '1'
|
||||
})
|
||||
} else {
|
||||
feedback.msgError(`最多添加${props.max}个`)
|
||||
}
|
||||
}
|
||||
const handleDelete = (index: number) => {
|
||||
if (props.modelValue?.length <= props.min) {
|
||||
return feedback.msgError(`最少保留${props.min}个`)
|
||||
}
|
||||
navLists.value.splice(index, 1)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<el-image :style="styles" v-bind="props" :src="getImageUrl(src)">
|
||||
<template #placeholder>
|
||||
<div class="image-slot"></div>
|
||||
</template>
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<icon name="el-icon-Picture" :size="30" />
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { imageProps } from 'element-plus'
|
||||
import type { CSSProperties } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
import { addUnit } from '@/utils/util'
|
||||
|
||||
const props = defineProps({
|
||||
width: {
|
||||
type: [String, Number],
|
||||
default: 'auto'
|
||||
},
|
||||
height: {
|
||||
type: [String, Number],
|
||||
default: 'auto'
|
||||
},
|
||||
radius: {
|
||||
type: [String, Number],
|
||||
default: 0
|
||||
},
|
||||
...imageProps
|
||||
})
|
||||
|
||||
const { getImageUrl } = useAppStore()
|
||||
const styles = computed<CSSProperties>(() => {
|
||||
return {
|
||||
width: addUnit(props.width),
|
||||
height: addUnit(props.height),
|
||||
borderRadius: addUnit(props.radius)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-image {
|
||||
display: block;
|
||||
.image-slot {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #fafafa;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<div class="pages-setting">
|
||||
<el-card shadow="never" class="!border-none flex">
|
||||
<div
|
||||
class="title flex items-center before:w-[3px] before:h-[14px] before:block before:bg-primary before:mr-2 text-xl font-medium"
|
||||
>
|
||||
{{ widget?.title }}
|
||||
</div>
|
||||
</el-card>
|
||||
<el-scrollbar class="w-full" style="height: calc(100% - 60px)">
|
||||
<keep-alive>
|
||||
<component
|
||||
:is="widgets[widget?.name]?.attr"
|
||||
:content="widget?.content"
|
||||
:styles="widget?.styles"
|
||||
:type="type"
|
||||
@update:content="handleUpdateContent"
|
||||
/>
|
||||
</keep-alive>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
import widgets from '../widgets'
|
||||
|
||||
const emits = defineEmits(['update:content'])
|
||||
const handleUpdateContent = (data: any) => {
|
||||
emits('update:content', data)
|
||||
}
|
||||
|
||||
defineProps({
|
||||
widget: {
|
||||
type: Object as PropType<Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
type: {
|
||||
type: String as PropType<'mobile' | 'pc'>,
|
||||
default: 'mobile'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div class="pages-menu">
|
||||
<el-menu
|
||||
:default-active="modelValue"
|
||||
class="w-[160px] min-h-[668px]"
|
||||
@select="handleSelect"
|
||||
>
|
||||
<el-menu-item v-for="(item, key) in menus" :index="key" :key="item.id">
|
||||
<span>{{ item.name }}</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
defineProps({
|
||||
menus: {
|
||||
type: Object as PropType<Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: '1'
|
||||
}
|
||||
})
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', value: string): void
|
||||
}>()
|
||||
const handleSelect = (index: string) => {
|
||||
emit('update:modelValue', index)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pages-menu {
|
||||
:deep(.el-menu) {
|
||||
border-right: none;
|
||||
}
|
||||
:deep(.el-menu-item) {
|
||||
border-color: transparent;
|
||||
&.is-active {
|
||||
border-right-width: 2px;
|
||||
border-color: var(--el-color-primary);
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<div class="pages-preview">
|
||||
<div class="relative flex justify-center h-full mt-5 mx-10">
|
||||
<!-- iframe预览 -->
|
||||
<iframe
|
||||
v-if="$route.query.url"
|
||||
ref="previewIframeRef"
|
||||
class="flex-1 h-full"
|
||||
width="100%"
|
||||
height="100%"
|
||||
scrolling="no"
|
||||
:src="$route.query.url as string"
|
||||
></iframe>
|
||||
<div class="max-w-[1200px] w-full absolute">
|
||||
<div
|
||||
v-for="(widget, index) in pageData"
|
||||
:key="widget.id"
|
||||
class="absolute left-0 top-0"
|
||||
:class="{
|
||||
'cursor-pointer': !widget?.disabled
|
||||
}"
|
||||
@click="handleClick(widget, index)"
|
||||
>
|
||||
<div
|
||||
class="absolute w-full h-full z-[100] border-dashed"
|
||||
:class="{
|
||||
select: index == modelValue,
|
||||
'border-[#dcdfe6] border-2': !widget?.disabled,
|
||||
hide: canShowCom(widget.content)
|
||||
}"
|
||||
:style="widget.styles"
|
||||
></div>
|
||||
<slot>
|
||||
<component
|
||||
:is="widgets[widget?.name]?.content"
|
||||
:content="widget.content"
|
||||
:styles="widget.styles"
|
||||
:key="widget.id"
|
||||
ref="commonComponentRef"
|
||||
/>
|
||||
</slot>
|
||||
<!-- 部件操作按钮组 -->
|
||||
<div
|
||||
class="widget-btns py-[5px]"
|
||||
v-if="index == modelValue"
|
||||
:style="{
|
||||
top: widget.styles.top,
|
||||
left: widget.styles.width
|
||||
}"
|
||||
>
|
||||
<div>
|
||||
<el-tooltip effect="dark" content="编辑组件内容" placement="right">
|
||||
<el-button
|
||||
class="py-[5px]"
|
||||
type="primary"
|
||||
:icon="Setting"
|
||||
@click="handleClickSetting(index)"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div>
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
:content="canShowCom(widget.content) ? '显示' : '隐藏'"
|
||||
placement="right"
|
||||
>
|
||||
<el-button
|
||||
class="py-[5px]"
|
||||
type="primary"
|
||||
:icon="canShowCom(widget.content) ? View : Hide"
|
||||
@click="changeShowCom(widget.content)"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { Hide, Setting, View } from '@element-plus/icons-vue'
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
import widgets from '../widgets'
|
||||
|
||||
const commonComponentRef = shallowRef<any>()
|
||||
|
||||
defineProps({
|
||||
pageData: {
|
||||
type: Array as PropType<any[]>,
|
||||
default: () => []
|
||||
},
|
||||
modelValue: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', value: number): void
|
||||
}>()
|
||||
|
||||
// 是否显示组件
|
||||
const canShowCom = computed(() => {
|
||||
return (data: any) => {
|
||||
return data?.enabled == 0
|
||||
}
|
||||
})
|
||||
|
||||
// 点击了组件设置
|
||||
const handleClickSetting = (index: number) => {
|
||||
commonComponentRef.value[index]?.open()
|
||||
}
|
||||
|
||||
// 修改组件显示/隐藏
|
||||
const changeShowCom = (data: any) => {
|
||||
if (data.enabled === undefined) return
|
||||
data.enabled = data.enabled ? 0 : 1
|
||||
}
|
||||
|
||||
const handleClick = (widget: any, index: number) => {
|
||||
if (widget.disabled) return
|
||||
emit('update:modelValue', index)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pages-preview {
|
||||
@apply w-full h-full relative;
|
||||
height: 85vh;
|
||||
|
||||
.select {
|
||||
@apply border-primary border-solid;
|
||||
}
|
||||
|
||||
.hide::before {
|
||||
content: '已隐藏';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.widget-btns {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
|
||||
width: 46px;
|
||||
border-radius: 8px;
|
||||
@apply bg-primary;
|
||||
margin-left: 10px;
|
||||
|
||||
:deep(.el-button) {
|
||||
width: 46px;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,220 @@
|
||||
<template>
|
||||
<el-scrollbar class="pages-preview-container">
|
||||
<div v-if="pageMeta !== null" class="absolute right-4 top-4" @click="handleClickPageMeta">
|
||||
<el-button>页面设置</el-button>
|
||||
</div>
|
||||
<div class="shadow mx-[30px] pages-preview">
|
||||
<div
|
||||
v-for="(widget, index) in pageData"
|
||||
:key="widget.id"
|
||||
class="relative"
|
||||
:class="{
|
||||
'cursor-pointer': !widget?.disabled
|
||||
}"
|
||||
@click="handleClick(widget, index)"
|
||||
>
|
||||
<!-- 选中的边框 -->
|
||||
<div
|
||||
class="absolute w-full h-full z-[100] border-dashed"
|
||||
:class="{
|
||||
select: index == modelValue,
|
||||
hide: canShowCom(widget.content),
|
||||
'border-[#dcdfe6] border-2': !widget?.disabled
|
||||
}"
|
||||
></div>
|
||||
<!-- 选中的组件 -->
|
||||
<slot>
|
||||
<component
|
||||
:is="widgets[widget?.name]?.content"
|
||||
:content="widget.content"
|
||||
:styles="widget.styles"
|
||||
:key="widget.id"
|
||||
/>
|
||||
</slot>
|
||||
<!-- 部件操作按钮组 -->
|
||||
<div class="widget-btns py-[5px]" v-if="index == modelValue">
|
||||
<div>
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
:content="canShowCom(widget.content) ? '显示' : '隐藏'"
|
||||
placement="right"
|
||||
>
|
||||
<el-button
|
||||
class="py-[5px]"
|
||||
type="primary"
|
||||
:icon="canShowCom(widget.content) ? View : Hide"
|
||||
@click="changeShowCom(widget.content)"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div>
|
||||
<el-tooltip effect="dark" content="上移" placement="right">
|
||||
<el-button
|
||||
class="py-[5px]"
|
||||
type="primary"
|
||||
:icon="ArrowUpBold"
|
||||
:disabled="canMoveUpCom(index)"
|
||||
@click.stop="rearrangeArray(index, index - 1)"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div>
|
||||
<el-tooltip effect="dark" content="下移" placement="right">
|
||||
<el-button
|
||||
class="py-[5px]"
|
||||
type="primary"
|
||||
:icon="ArrowDownBold"
|
||||
:disabled="canMoveDownCom(index)"
|
||||
@click.stop="rearrangeArray(index, index + 1)"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ArrowDownBold, ArrowUpBold, Hide, View } from '@element-plus/icons-vue'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import type { PropType } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import widgets from '../widgets'
|
||||
|
||||
const props = defineProps({
|
||||
pageMeta: {
|
||||
type: Object as any,
|
||||
default: () => null
|
||||
},
|
||||
pageData: {
|
||||
type: Array as PropType<any[]>,
|
||||
default: () => []
|
||||
},
|
||||
modelValue: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', value: number): void
|
||||
(event: 'updatePageData', value: any[]): void
|
||||
}>()
|
||||
|
||||
const oldModelValue = ref<number>(-1)
|
||||
|
||||
const handleClickPageMeta = () => {
|
||||
if (props.modelValue === -1) {
|
||||
emit('update:modelValue', oldModelValue.value)
|
||||
} else {
|
||||
oldModelValue.value = props.modelValue
|
||||
emit('update:modelValue', -1)
|
||||
}
|
||||
}
|
||||
|
||||
const handleClick = (widget: any, index: number) => {
|
||||
if (widget.disabled) return
|
||||
emit('update:modelValue', index)
|
||||
}
|
||||
|
||||
// 是否可以移动组件
|
||||
const canMoveUpCom = computed(() => {
|
||||
return (index: number) => {
|
||||
return index === 0
|
||||
}
|
||||
})
|
||||
|
||||
// 是否可以移动组件
|
||||
const canMoveDownCom = computed(() => {
|
||||
return (index: number) => {
|
||||
return props.pageData?.length === index + 1
|
||||
}
|
||||
})
|
||||
|
||||
// 是否显示组件
|
||||
const canShowCom = computed(() => {
|
||||
return (data: any) => {
|
||||
return data?.enabled == 0
|
||||
}
|
||||
})
|
||||
|
||||
// 修改组件显示/隐藏
|
||||
const changeShowCom = (data: any) => {
|
||||
if (data.enabled === undefined) return
|
||||
data.enabled = data.enabled ? 0 : 1
|
||||
}
|
||||
|
||||
const rearrangeArray = (currentIdx: number, targetIdx: number) => {
|
||||
if (
|
||||
currentIdx < 0 ||
|
||||
currentIdx >= props.pageData.length ||
|
||||
targetIdx < 0 ||
|
||||
targetIdx >= props.pageData.length
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
// const element = props.pageData.splice(currentIdx, 1)[0]
|
||||
// props.pageData.splice(targetIdx, 0, element)
|
||||
const newPageData = cloneDeep(props.pageData)
|
||||
const element = newPageData.splice(currentIdx, 1)[0]
|
||||
newPageData.splice(targetIdx, 0, element)
|
||||
|
||||
emit('updatePageData', newPageData)
|
||||
emit('update:modelValue', targetIdx)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pages-preview-container {
|
||||
position: relative;
|
||||
:deep(.el-scrollbar__wrap) {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pages-preview {
|
||||
background-color: #f8f8f8;
|
||||
width: 360px;
|
||||
min-height: 615px;
|
||||
color: #333;
|
||||
|
||||
.select {
|
||||
@apply border-primary border-solid;
|
||||
}
|
||||
|
||||
.hide::before {
|
||||
content: '已隐藏';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.widget-btns {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: -60px;
|
||||
overflow: hidden;
|
||||
|
||||
width: 46px;
|
||||
border-radius: 8px;
|
||||
@apply bg-primary;
|
||||
|
||||
:deep(.el-button) {
|
||||
width: 46px;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,183 @@
|
||||
<template>
|
||||
<el-card shadow="never" class="!border-none flex">
|
||||
<div
|
||||
class="title flex items-center before:w-[3px] before:h-[14px] before:block before:bg-primary before:mr-2 text-xl font-medium"
|
||||
>
|
||||
底部导航设置
|
||||
<span class="form-tips ml-[10px] !mt-0"> 至少添加2个导航,最多添加5个导航 </span>
|
||||
</div>
|
||||
</el-card>
|
||||
<el-form label-width="70px">
|
||||
<el-card shadow="never" class="!border-none flex mt-2">
|
||||
<div class="flex items-end mb-4">
|
||||
<div class="text-base text-[#101010] font-medium">展示样式</div>
|
||||
</div>
|
||||
<el-form-item label="默认颜色">
|
||||
<color-picker
|
||||
class="max-w-[400px]"
|
||||
v-model="data.style.default_color"
|
||||
default-color="#999999"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="选中颜色" style="margin-bottom: 0">
|
||||
<color-picker
|
||||
class="max-w-[400px]"
|
||||
v-model="data.style.selected_color"
|
||||
default-color="#4173ff"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
<el-card shadow="never" class="!border-none flex mt-2">
|
||||
<div class="flex items-end mb-4">
|
||||
<div class="text-base text-[#101010] font-medium">菜单设置</div>
|
||||
<div class="text-xs text-tx-secondary ml-2">建议图片尺寸:100px*100px</div>
|
||||
</div>
|
||||
<div class="mb-[18px] max-w-[400px]">
|
||||
<Draggable
|
||||
class="draggable"
|
||||
v-model="data.list"
|
||||
animation="300"
|
||||
draggable=".draggable"
|
||||
handle=".drag-move"
|
||||
:move="onMove"
|
||||
item-key="index"
|
||||
>
|
||||
<template v-slot:item="{ element, index }">
|
||||
<del-wrap
|
||||
@close="handleDelete(index)"
|
||||
class="max-w-[400px]"
|
||||
:show-close="index !== 0"
|
||||
:class="{ draggable: index != 0 }"
|
||||
>
|
||||
<div class="bg-fill-light w-full p-4 mt-4">
|
||||
<el-form-item label="导航图标">
|
||||
<material-picker
|
||||
v-model="element.unselected"
|
||||
upload-class="bg-body"
|
||||
exclude-domain
|
||||
size="60px"
|
||||
>
|
||||
<template #upload>
|
||||
<div class="upload-btn w-[60px] h-[60px]">
|
||||
<icon name="el-icon-Plus" :size="16" />
|
||||
<span class="text-xs leading-5"> 未选中 </span>
|
||||
</div>
|
||||
</template>
|
||||
</material-picker>
|
||||
<material-picker
|
||||
v-model="element.selected"
|
||||
exclude-domain
|
||||
upload-class="bg-body"
|
||||
size="60px"
|
||||
>
|
||||
<template #upload>
|
||||
<div class="upload-btn w-[60px] h-[60px]">
|
||||
<icon name="el-icon-Plus" :size="16" />
|
||||
<span class="text-xs leading-5"> 选中 </span>
|
||||
</div>
|
||||
</template>
|
||||
</material-picker>
|
||||
</el-form-item>
|
||||
<el-form-item label="导航名称">
|
||||
<el-input v-model="element.name" placeholder="请输入名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="链接地址">
|
||||
<link-picker
|
||||
:is-tab="true"
|
||||
:disabled="index === 0"
|
||||
v-model="element.link"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否显示">
|
||||
<div class="flex-1 flex items-center">
|
||||
<el-switch
|
||||
:disabled="index == 0"
|
||||
v-model="element.is_show"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
@change="handleShowChange(element)"
|
||||
/>
|
||||
<div class="drag-move cursor-move ml-auto">
|
||||
<icon name="el-icon-Rank" size="18" />
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</del-wrap>
|
||||
</template>
|
||||
</Draggable>
|
||||
</div>
|
||||
|
||||
<div v-if="data.list?.length < max" class="mt-4">
|
||||
<el-button class="w-full" type="primary" @click="handleAdd">
|
||||
添加导航 {{ data.list?.length }} / {{ max }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-form>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import Draggable from 'vuedraggable'
|
||||
|
||||
import feedback from '@/utils/feedback'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
list: [],
|
||||
style: {}
|
||||
})
|
||||
}
|
||||
})
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', value: any): void
|
||||
}>()
|
||||
const data = computed({
|
||||
get() {
|
||||
return props.modelValue
|
||||
},
|
||||
set(value) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
|
||||
const max = 5
|
||||
const min = 2
|
||||
const showTabbarList = computed(() => {
|
||||
return data.value.list?.filter((tab: any) => tab.is_show == '1') || []
|
||||
})
|
||||
const handleAdd = () => {
|
||||
if (data.value.list?.length < max) {
|
||||
data.value.list.push({
|
||||
name: '',
|
||||
selected: '',
|
||||
unselected: '',
|
||||
is_show: 1,
|
||||
link: {}
|
||||
})
|
||||
} else {
|
||||
feedback.msgError(`最多添加${max}个`)
|
||||
}
|
||||
}
|
||||
const handleDelete = (index: number) => {
|
||||
if (data.value.list?.length <= min) {
|
||||
return feedback.msgError(`最少保留${min}个`)
|
||||
}
|
||||
data.value.list.splice(index, 1)
|
||||
}
|
||||
|
||||
const onMove = (e: any) => {
|
||||
if (e.relatedContext.index == 0) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const handleShowChange = (item: any) => {
|
||||
if (showTabbarList.value.length < min) {
|
||||
item.is_show = 1
|
||||
return feedback.msgError(`最少显示${min}个`)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div class="tabbar flex">
|
||||
<template v-for="(item, index) in showTabbarList" :key="index">
|
||||
<div
|
||||
class="tabbar-item flex flex-col justify-center items-center flex-1"
|
||||
:style="{ color: style.default_color }"
|
||||
>
|
||||
<decoration-img width="22px" height="22px" :src="item.unselected" fit="cover" />
|
||||
<div class="leading-3 text-[12px] mt-[4px]">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
import DecorationImg from '../../decoration-img.vue'
|
||||
|
||||
const props = defineProps({
|
||||
style: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
list: {
|
||||
type: Array as PropType<any[]>,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
const showTabbarList = computed(() => {
|
||||
return props.list?.filter((tab: any) => tab.is_show == 1) || []
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.tabbar {
|
||||
position: absolute;
|
||||
height: 50px;
|
||||
background-color: #fff;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
border: 2px solid var(--el-color-primary);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,7 @@
|
||||
import attr from './attr.vue'
|
||||
import content from './content.vue'
|
||||
|
||||
export default {
|
||||
attr,
|
||||
content
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<div
|
||||
class="title flex items-center before:w-[3px] before:h-[14px] before:block before:bg-primary before:mr-2"
|
||||
>
|
||||
pc导航设置
|
||||
</div>
|
||||
<el-form class="mt-4" label-width="70px">
|
||||
<el-tabs model-value="nav">
|
||||
<el-tab-pane label="主导航设置" name="nav">
|
||||
<menu-set v-model="data.nav" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="菜单设置" name="menu">
|
||||
<menu-set v-model="data.menu" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-form>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import MenuSet from './menu-set.vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
nav: [],
|
||||
menu: {}
|
||||
})
|
||||
}
|
||||
})
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', value: any): void
|
||||
}>()
|
||||
const data = computed({
|
||||
get() {
|
||||
return props.modelValue
|
||||
},
|
||||
set(value) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<div class="pc-aside absolute top-0 bottom-0 left-0 w-[150px]">
|
||||
<ElScrollbar>
|
||||
<div class="h-full px-[10px] flex flex-col">
|
||||
<div class="mb-auto">
|
||||
<div class="nav">
|
||||
<template v-for="(item, index) in nav" :key="index">
|
||||
<div
|
||||
v-if="item.is_show == '1'"
|
||||
:to="item.link.path"
|
||||
class="nav-item"
|
||||
:class="{
|
||||
active: index == 0
|
||||
}"
|
||||
>
|
||||
<decoration-img
|
||||
width="18px"
|
||||
height="18px"
|
||||
:src="index == 0 ? item.selected : item.unselected"
|
||||
fit="cover"
|
||||
>
|
||||
<template #error>
|
||||
<span></span>
|
||||
</template>
|
||||
</decoration-img>
|
||||
|
||||
<div class="ml-[10px]">{{ item.name }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="menu">
|
||||
<div class="menu-item" v-for="(item, index) in menu" :key="index">
|
||||
<decoration-img
|
||||
width="16px"
|
||||
height="16px"
|
||||
:src="item.unselected"
|
||||
fit="cover"
|
||||
>
|
||||
<template #error>
|
||||
<span></span>
|
||||
</template>
|
||||
</decoration-img>
|
||||
|
||||
<span class="ml-[6px] line-clamp-1">{{ item.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElScrollbar>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
import DecorationImg from '../../decoration-img.vue'
|
||||
|
||||
defineProps({
|
||||
nav: {
|
||||
type: Array as PropType<any[]>,
|
||||
default: () => []
|
||||
},
|
||||
menu: {
|
||||
type: Array as PropType<any[]>,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.pc-aside {
|
||||
border: 2px solid var(--el-color-primary);
|
||||
:deep(.el-scrollbar__view) {
|
||||
height: 100%;
|
||||
}
|
||||
.nav {
|
||||
.nav-item {
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
margin: 10px 0;
|
||||
padding: 0 12px;
|
||||
&.active {
|
||||
color: #fff;
|
||||
background: linear-gradient(90deg, #19e8b7 0%, #00abff 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 38px;
|
||||
line-height: 38px;
|
||||
padding: 0 12px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
margin: 3px 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,7 @@
|
||||
import attr from './attr.vue'
|
||||
import content from './content.vue'
|
||||
|
||||
export default {
|
||||
attr,
|
||||
content
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<div class="mb-[18px] max-w-[400px]">
|
||||
<Draggable
|
||||
class="draggable"
|
||||
v-model="menu"
|
||||
animation="300"
|
||||
handle=".drag-move"
|
||||
item-key="index"
|
||||
>
|
||||
<template v-slot:item="{ element, index }">
|
||||
<del-wrap @close="handleDelete(index)" class="max-w-[400px]">
|
||||
<div class="bg-fill-light w-full p-4 mt-4">
|
||||
<el-form-item
|
||||
label="导航图标"
|
||||
v-if="
|
||||
element.unselected !== undefined || element.selected !== undefined
|
||||
"
|
||||
>
|
||||
<material-picker
|
||||
v-if="element.unselected !== undefined"
|
||||
v-model="element.unselected"
|
||||
upload-class="bg-body"
|
||||
exclude-domain
|
||||
size="60px"
|
||||
>
|
||||
<template #upload>
|
||||
<div class="upload-btn w-[60px] h-[60px]">
|
||||
<icon name="el-icon-Plus" :size="16" />
|
||||
<span class="text-xs leading-5"> 未选中 </span>
|
||||
</div>
|
||||
</template>
|
||||
</material-picker>
|
||||
<material-picker
|
||||
v-model="element.selected"
|
||||
v-if="element.selected !== undefined"
|
||||
upload-class="bg-body"
|
||||
exclude-domain
|
||||
size="60px"
|
||||
>
|
||||
<template #upload>
|
||||
<div class="upload-btn w-[60px] h-[60px]">
|
||||
<icon name="el-icon-Plus" :size="16" />
|
||||
<span class="text-xs leading-5"> 选中 </span>
|
||||
</div>
|
||||
</template>
|
||||
</material-picker>
|
||||
</el-form-item>
|
||||
<el-form-item label="导航名称" v-if="element.name !== undefined">
|
||||
<el-input v-model="element.name" placeholder="请输入名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="链接地址" v-if="element.link !== undefined">
|
||||
<link-picker v-model="element.link" type="pc" />
|
||||
</el-form-item>
|
||||
<el-form-item label="是否显示" v-if="element.is_show !== undefined">
|
||||
<div class="flex-1 flex items-center">
|
||||
<el-switch
|
||||
v-model="element.is_show"
|
||||
active-value="1"
|
||||
inactive-value="0"
|
||||
/>
|
||||
<div class="drag-move cursor-move ml-auto">
|
||||
<icon name="el-icon-Rank" size="18" />
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</del-wrap>
|
||||
</template>
|
||||
</Draggable>
|
||||
</div>
|
||||
|
||||
<el-form-item v-if="menu.length < max" label-width="0">
|
||||
<el-button type="primary" @click="handleAdd"> 添加导航 </el-button>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import Draggable from 'vuedraggable'
|
||||
|
||||
import feedback from '@/utils/feedback'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: any[]
|
||||
max?: number
|
||||
min?: number
|
||||
itemData: any
|
||||
}>(),
|
||||
{
|
||||
max: 9999,
|
||||
min: -1,
|
||||
itemData: () => ({
|
||||
name: '',
|
||||
selected: '',
|
||||
unselected: '',
|
||||
is_show: '1',
|
||||
link: {}
|
||||
})
|
||||
}
|
||||
)
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', value: any): void
|
||||
}>()
|
||||
const menu = computed({
|
||||
get() {
|
||||
return props.modelValue
|
||||
},
|
||||
set(value) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
|
||||
const handleAdd = () => {
|
||||
if (menu.value?.length < props.max) {
|
||||
menu.value.push(cloneDeep(props.itemData))
|
||||
} else {
|
||||
feedback.msgError(`最多添加${props.max}个`)
|
||||
}
|
||||
}
|
||||
const handleDelete = (index: number) => {
|
||||
if (menu.value.length <= props.min && props.min > -1) {
|
||||
return feedback.msgError(`最少保留${props.min}个`)
|
||||
}
|
||||
menu.value.splice(index, 1)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,180 @@
|
||||
<template>
|
||||
<el-form label-width="70px">
|
||||
<el-card shadow="never" class="!border-none flex mt-2">
|
||||
<div class="flex items-end mb-4">
|
||||
<div class="text-base dark:text-[#ffffff] text-[#101010] font-medium">展示样式</div>
|
||||
</div>
|
||||
<el-radio-group v-model="contentData.style">
|
||||
<el-radio :value="1">常规</el-radio>
|
||||
<el-radio :value="2">大屏</el-radio>
|
||||
</el-radio-group>
|
||||
</el-card>
|
||||
<el-card shadow="never" class="!border-none flex mt-2" v-if="content.style == 1">
|
||||
<div class="flex items-end mb-4">
|
||||
<div class="text-base text-[#101010] dark:text-[#ffffff] font-medium">背景联动</div>
|
||||
</div>
|
||||
<el-radio-group v-model="contentData.bg_style">
|
||||
<el-radio :value="1">开启</el-radio>
|
||||
<el-radio :value="0">关闭</el-radio>
|
||||
</el-radio-group>
|
||||
<div class="p-[15px] rounded-[8px] bg-[#f3f8ff] text-[#136bdf] mt-2">
|
||||
开启背景联动后,需为轮播图设置背景图,轮播图切换时,背景图也跟随切换,此时该页面自身的“页面背景“设置将失效。
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card shadow="never" class="!border-none flex-1 mt-2">
|
||||
<div class="flex items-end">
|
||||
<div class="text-base text-[#101010] dark:text-[#ffffff] font-medium">轮播图片</div>
|
||||
<div v-if="content.style == 1" class="text-xs text-tx-secondary ml-2">
|
||||
最多添加5张,建议图片尺寸:750px*340px
|
||||
</div>
|
||||
<div v-else class="text-xs text-tx-secondary ml-2">
|
||||
最多添加5张,建议图片尺寸:750px*1100px
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<draggable
|
||||
class="draggable"
|
||||
v-model="contentData.data"
|
||||
animation="300"
|
||||
handle=".drag-move"
|
||||
item-key="index"
|
||||
>
|
||||
<template v-slot:item="{ element: item, index }">
|
||||
<del-wrap :key="index" @close="handleDelete(index)" class="w-full">
|
||||
<div class="bg-fill-light w-full p-4 mt-4">
|
||||
<div class="flex justify-center w-[467px]">
|
||||
<template v-if="content.style == 1">
|
||||
<material-picker
|
||||
size="122px"
|
||||
v-model="item.image"
|
||||
upload-class="bg-body"
|
||||
exclude-domain
|
||||
>
|
||||
<template #upload>
|
||||
<div class="w-[122px] h-[122px] banner-upload-btn">
|
||||
轮播图
|
||||
</div>
|
||||
</template>
|
||||
</material-picker>
|
||||
<material-picker
|
||||
v-if="content.style == 1 || content.bg_style == 1"
|
||||
class="ml-[40px]"
|
||||
size="122px"
|
||||
v-model="item.bg"
|
||||
upload-class="bg-body"
|
||||
exclude-domain
|
||||
>
|
||||
<template #upload>
|
||||
<div class="w-[122px] h-[122px] banner-upload-btn">
|
||||
背景图
|
||||
</div>
|
||||
</template>
|
||||
</material-picker>
|
||||
</template>
|
||||
<template v-else>
|
||||
<material-picker
|
||||
width="396px"
|
||||
height="196px"
|
||||
v-model="item.image"
|
||||
upload-class="bg-body"
|
||||
exclude-domain
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<el-form-item class="mt-[18px]" label="图片链接">
|
||||
<link-picker v-if="type == 'mobile'" v-model="item.link" />
|
||||
<el-input
|
||||
v-if="type == 'pc'"
|
||||
placeholder="请输入链接"
|
||||
v-model="item.link.path"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否显示" class="mt-[18px] !mb-0">
|
||||
<div class="flex-1 flex items-center">
|
||||
<el-switch
|
||||
v-model="item.is_show"
|
||||
active-value="1"
|
||||
inactive-value="0"
|
||||
/>
|
||||
<div class="drag-move cursor-move ml-auto">
|
||||
<icon name="el-icon-Rank" size="18" />
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</del-wrap>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
<div class="mt-4" v-if="content.data?.length < limit">
|
||||
<el-button class="w-full" type="primary" @click="handleAdd">添加图片</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-form>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import type { PropType } from 'vue'
|
||||
import Draggable from 'vuedraggable'
|
||||
|
||||
import feedback from '@/utils/feedback'
|
||||
|
||||
import type options from './options'
|
||||
|
||||
type OptionsType = ReturnType<typeof options>
|
||||
const emits = defineEmits<(event: 'update:content', data: OptionsType['content']) => void>()
|
||||
const limit = 5
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: Object as PropType<OptionsType['content']>,
|
||||
default: () => ({})
|
||||
},
|
||||
styles: {
|
||||
type: Object as PropType<OptionsType['styles']>,
|
||||
default: () => ({})
|
||||
},
|
||||
type: {
|
||||
type: String as PropType<'mobile' | 'pc'>,
|
||||
default: 'mobile'
|
||||
}
|
||||
})
|
||||
|
||||
const contentData = computed({
|
||||
get: () => props.content,
|
||||
set: (newValue) => {
|
||||
emits('update:content', newValue)
|
||||
}
|
||||
})
|
||||
|
||||
const handleAdd = () => {
|
||||
if (props.content.data?.length < limit) {
|
||||
const content = cloneDeep(props.content)
|
||||
content.data.push({
|
||||
is_show: '1',
|
||||
image: '',
|
||||
bg: '',
|
||||
name: '',
|
||||
link: {}
|
||||
})
|
||||
emits('update:content', content)
|
||||
} else {
|
||||
feedback.msgError(`最多添加${limit}张图片`)
|
||||
}
|
||||
}
|
||||
const handleDelete = (index: number) => {
|
||||
if (props.content.data?.length <= 1) {
|
||||
return feedback.msgError('最少保留一张图片')
|
||||
}
|
||||
const content = cloneDeep(props.content)
|
||||
content.data.splice(index, 1)
|
||||
emits('update:content', content)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.banner-upload-btn {
|
||||
@apply text-tx-secondary box-border rounded border-br border-dashed border flex flex-col justify-center items-center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div class="banner" :style="styles">
|
||||
<div class="banner-image w-full h-full">
|
||||
<decoration-img
|
||||
width="100%"
|
||||
:height="content.style == 1 ? height : '550px'"
|
||||
:src="getImage"
|
||||
fit="contain"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
import DecorationImg from '../../decoration-img.vue'
|
||||
import type options from './options'
|
||||
|
||||
type OptionsType = ReturnType<typeof options>
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: Object as PropType<OptionsType['content']>,
|
||||
default: () => ({})
|
||||
},
|
||||
styles: {
|
||||
type: Object as PropType<OptionsType['styles']>,
|
||||
default: () => ({})
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '170px'
|
||||
}
|
||||
})
|
||||
|
||||
const showList = computed(() => {
|
||||
return props.content.data?.filter((item: any) => item.is_show == '1') || []
|
||||
})
|
||||
|
||||
const getImage = computed(() => {
|
||||
if (Array.isArray(showList.value)) {
|
||||
return showList.value[0] ? showList.value[0].image : ''
|
||||
}
|
||||
return ''
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,9 @@
|
||||
import attr from './attr.vue'
|
||||
import content from './content.vue'
|
||||
import options from './options'
|
||||
|
||||
export default {
|
||||
attr,
|
||||
content,
|
||||
options
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
export default () => ({
|
||||
title: '首页轮播图',
|
||||
name: 'banner',
|
||||
content: {
|
||||
enabled: 1,
|
||||
style: 1, // 展示样式---1=常规,2=大屏
|
||||
bg_style: 0, // 背景联动---0=关闭,1=开启
|
||||
data: [
|
||||
{
|
||||
is_show: '1',
|
||||
image: '',
|
||||
bg: '',
|
||||
name: '',
|
||||
link: {}
|
||||
}
|
||||
]
|
||||
},
|
||||
styles: {}
|
||||
})
|
||||
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form label-width="90px" size="large" label-position="top">
|
||||
<el-card shadow="never" class="!border-none flex mt-2">
|
||||
<el-form-item label="平台名称">
|
||||
<el-input
|
||||
class="w-[400px]"
|
||||
show-word-limit
|
||||
maxlength="20"
|
||||
v-model="contentData.title"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="客服二维码">
|
||||
<div>
|
||||
<material-picker v-model="contentData.qrcode" exclude-domain />
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="备注">
|
||||
<el-input
|
||||
class="w-[400px]"
|
||||
show-word-limit
|
||||
maxlength="20"
|
||||
v-model="contentData.remark"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="联系电话">
|
||||
<el-input class="w-[400px]" v-model="contentData.mobile" />
|
||||
</el-form-item>
|
||||
<el-form-item label="服务时间">
|
||||
<el-input
|
||||
class="w-[400px]"
|
||||
show-word-limit
|
||||
maxlength="20"
|
||||
v-model="contentData.time"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
import type options from './options'
|
||||
|
||||
type OptionsType = ReturnType<typeof options>
|
||||
const emits = defineEmits<(event: 'update:content', data: OptionsType['content']) => void>()
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: Object as PropType<OptionsType['content']>,
|
||||
default: () => ({})
|
||||
},
|
||||
styles: {
|
||||
type: Object as PropType<OptionsType['styles']>,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
const contentData = computed({
|
||||
get: () => props.content,
|
||||
set: (newValue) => {
|
||||
emits('update:content', newValue)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<view class="bg-white p-[15px] flex text-[#101010] font-medium text-lg"> 联系我们 </view>
|
||||
<view
|
||||
class="customer-service bg-white flex flex-col justify-center items-center mx-[18px] mt-[15px] rounded-[10px] px-[10px] pb-[50px]"
|
||||
>
|
||||
<view
|
||||
class="w-full border-solid border-0 border-b border-[#f5f5f5] p-[15px] text-center text-[#101010] text-base font-medium"
|
||||
>
|
||||
{{ content.title }}
|
||||
</view>
|
||||
|
||||
<view class="mt-[30px]">
|
||||
<decoration-img width="100px" height="100px" :src="content.qrcode" alt="" />
|
||||
</view>
|
||||
<view v-if="content.remark" class="text-sm mt-[20px] font-medium">{{
|
||||
content.remark
|
||||
}}</view>
|
||||
<view v-if="content.mobile" class="text-sm mt-[12px] flex flex-wrap">
|
||||
<a class="ml-[5px] phone text-primary underline" :href="'tel:' + content.mobile">
|
||||
{{ content.mobile }}
|
||||
</a>
|
||||
</view>
|
||||
<view v-if="content.time" class="text-muted text-sm mt-[15px]">
|
||||
服务时间:{{ content.time }}
|
||||
</view>
|
||||
</view>
|
||||
Î
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
import DecorationImg from '../../decoration-img.vue'
|
||||
import type options from './options'
|
||||
|
||||
type OptionsType = ReturnType<typeof options>
|
||||
defineProps({
|
||||
content: {
|
||||
type: Object as PropType<OptionsType['content']>,
|
||||
default: () => ({})
|
||||
},
|
||||
styles: {
|
||||
type: Object as PropType<OptionsType['styles']>,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.customer-service {
|
||||
background: #fff;
|
||||
@apply flex flex-col justify-center items-center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,9 @@
|
||||
import attr from './attr.vue'
|
||||
import content from './content.vue'
|
||||
import options from './options'
|
||||
|
||||
export default {
|
||||
attr,
|
||||
content,
|
||||
options
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export default () => ({
|
||||
title: '客服设置',
|
||||
name: 'customer-service',
|
||||
content: {
|
||||
title: '添加客服二维码',
|
||||
time: '',
|
||||
mobile: '',
|
||||
qrcode: '',
|
||||
remark: ''
|
||||
},
|
||||
styles: {}
|
||||
})
|
||||
@@ -0,0 +1,14 @@
|
||||
const widgets: Record<string, any> = import.meta.glob('./**/index.ts', { eager: true })
|
||||
interface Widget {
|
||||
attr?: any
|
||||
content: any
|
||||
options: any
|
||||
}
|
||||
|
||||
const exportWidgets: Record<string, Widget> = {}
|
||||
Object.keys(widgets).forEach((key) => {
|
||||
const widgetName = key.replace(/^\.\/([\w-]+).*/gi, '$1')
|
||||
exportWidgets[widgetName] = widgets[key]?.default
|
||||
})
|
||||
|
||||
export default exportWidgets
|
||||
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form label-width="70px">
|
||||
<el-card shadow="never" class="!border-none flex mt-2">
|
||||
<div class="flex items-end">
|
||||
<div class="text-base text-[#101010] font-medium">图片设置</div>
|
||||
<div class="text-xs text-tx-secondary ml-2">
|
||||
最多添加5张,建议图片尺寸:750px*200px
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<draggable
|
||||
class="draggable"
|
||||
v-model="contentData.data"
|
||||
animation="300"
|
||||
handle=".drag-move"
|
||||
>
|
||||
<template v-slot:item="{ element: item, index }">
|
||||
<del-wrap :key="index" @close="handleDelete(index)" class="w-full">
|
||||
<div class="bg-fill-light w-full p-4 mt-4">
|
||||
<material-picker
|
||||
width="396px"
|
||||
height="196px"
|
||||
v-model="item.image"
|
||||
upload-class="bg-body"
|
||||
exclude-domain
|
||||
/>
|
||||
<div class="flex-1">
|
||||
<el-form-item class="mt-[18px]" label="图片链接">
|
||||
<link-picker
|
||||
v-if="type == 'mobile'"
|
||||
v-model="item.link"
|
||||
/>
|
||||
<el-input
|
||||
v-if="type == 'pc'"
|
||||
placeholder="请输入链接"
|
||||
v-model="item.link.path"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否显示" class="mt-[18px] !mb-0">
|
||||
<div class="flex-1 flex items-center">
|
||||
<el-switch
|
||||
v-model="item.is_show"
|
||||
active-value="1"
|
||||
inactive-value="0"
|
||||
/>
|
||||
<div class="drag-move cursor-move ml-auto">
|
||||
<icon name="el-icon-Rank" size="18" />
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</del-wrap>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
<div class="mt-4" v-if="content.data?.length < limit">
|
||||
<el-button class="w-full" type="primary" @click="handleAdd">添加图片</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import type { PropType } from 'vue'
|
||||
import Draggable from 'vuedraggable'
|
||||
|
||||
import feedback from '@/utils/feedback'
|
||||
|
||||
import type options from './options'
|
||||
|
||||
const limit = 5
|
||||
type OptionsType = ReturnType<typeof options>
|
||||
const emits = defineEmits<(event: 'update:content', data: OptionsType['content']) => void>()
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: Object as PropType<OptionsType['content']>,
|
||||
default: () => ({})
|
||||
},
|
||||
styles: {
|
||||
type: Object as PropType<OptionsType['styles']>,
|
||||
default: () => ({})
|
||||
},
|
||||
type: {
|
||||
type: String as PropType<'mobile' | 'pc'>,
|
||||
default: 'mobile'
|
||||
}
|
||||
})
|
||||
|
||||
const contentData = computed({
|
||||
get: () => props.content,
|
||||
set: (newValue) => {
|
||||
emits('update:content', newValue)
|
||||
}
|
||||
})
|
||||
|
||||
const handleAdd = () => {
|
||||
if (props.content.data?.length < limit) {
|
||||
const content = cloneDeep(props.content)
|
||||
content.data.push({
|
||||
is_show: '1',
|
||||
image: '',
|
||||
name: '',
|
||||
link: {}
|
||||
})
|
||||
emits('update:content', content)
|
||||
} else {
|
||||
feedback.msgError(`最多添加${limit}张图片`)
|
||||
}
|
||||
}
|
||||
const handleDelete = (index: number) => {
|
||||
if (props.content.data?.length <= 1) {
|
||||
return feedback.msgError('最少保留一张图片')
|
||||
}
|
||||
const content = cloneDeep(props.content)
|
||||
content.data.splice(index, 1)
|
||||
emits('update:content', content)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div class="banner" :style="styles">
|
||||
<div class="banner-image w-full h-full">
|
||||
<decoration-img
|
||||
width="100%"
|
||||
:height="styles.height || height"
|
||||
:src="getImage"
|
||||
fit="contain"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
import DecorationImg from '../../decoration-img.vue'
|
||||
import type options from './options'
|
||||
|
||||
type OptionsType = ReturnType<typeof options>
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: Object as PropType<OptionsType['content']>,
|
||||
default: () => ({})
|
||||
},
|
||||
styles: {
|
||||
type: Object as PropType<OptionsType['styles']>,
|
||||
default: () => ({})
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '96px'
|
||||
}
|
||||
})
|
||||
|
||||
const showList = computed(() => {
|
||||
return props.content.data?.filter((item: any) => item.is_show == '1') || []
|
||||
})
|
||||
|
||||
const getImage = computed(() => {
|
||||
if (Array.isArray(showList.value)) {
|
||||
return showList.value[0] ? showList.value[0].image : ''
|
||||
}
|
||||
return ''
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,9 @@
|
||||
import attr from './attr.vue'
|
||||
import content from './content.vue'
|
||||
import options from './options'
|
||||
|
||||
export default {
|
||||
attr,
|
||||
content,
|
||||
options
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export default () => ({
|
||||
title: '首页中部轮播图',
|
||||
name: 'middle-banner',
|
||||
content: {
|
||||
enabled: 1,
|
||||
data: [
|
||||
{
|
||||
is_show: '1',
|
||||
image: '',
|
||||
name: '',
|
||||
link: {}
|
||||
}
|
||||
]
|
||||
},
|
||||
styles: {}
|
||||
})
|
||||
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form label-width="70px">
|
||||
<el-card shadow="never" class="!border-none flex mt-2">
|
||||
<el-form-item label="标题">
|
||||
<el-input class="w-[396px]" v-model="contentData.title" />
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
<el-card shadow="never" class="!border-none flex mt-2">
|
||||
<div class="flex items-end mb-4">
|
||||
<div class="text-base text-[#101010] font-medium">展示样式</div>
|
||||
</div>
|
||||
<el-radio-group v-model="contentData.style">
|
||||
<el-radio :value="1">横排</el-radio>
|
||||
<el-radio :value="2">竖排</el-radio>
|
||||
</el-radio-group>
|
||||
</el-card>
|
||||
<el-card shadow="never" class="!border-none flex mt-2">
|
||||
<div class="flex items-end mb-4">
|
||||
<div class="text-base text-[#101010] font-medium">菜单</div>
|
||||
<div class="text-xs text-tx-secondary ml-2">建议图片尺寸:100px*100px</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<AddNav v-model="contentData.data" />
|
||||
</div>
|
||||
</el-card>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
import AddNav from '../../add-nav.vue'
|
||||
import type options from './options'
|
||||
|
||||
type OptionsType = ReturnType<typeof options>
|
||||
const emits = defineEmits<(event: 'update:content', data: OptionsType['content']) => void>()
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: Object as PropType<OptionsType['content']>,
|
||||
default: () => ({})
|
||||
},
|
||||
styles: {
|
||||
type: Object as PropType<OptionsType['styles']>,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
const contentData = computed({
|
||||
get: () => props.content,
|
||||
set: (newValue) => {
|
||||
emits('update:content', newValue)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div class="my-service">
|
||||
<div v-if="content.title" class="title px-[15px] py-[10px]">
|
||||
<div>{{ content.title }}</div>
|
||||
</div>
|
||||
<div v-if="content.style == 1" class="flex flex-wrap pt-[20px] pb-[10px]">
|
||||
<div
|
||||
v-for="(item, index) in showList"
|
||||
:key="index"
|
||||
class="flex flex-col items-center w-1/4 mb-[15px]"
|
||||
>
|
||||
<decoration-img width="26px" height="26px" :src="item.image" alt="" />
|
||||
<div class="mt-[7px]">{{ item.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="content.style == 2">
|
||||
<div
|
||||
v-for="(item, index) in showList"
|
||||
:key="index"
|
||||
class="flex items-center border-b border-[#e5e5e5] h-[50px] px-[12px]"
|
||||
>
|
||||
<decoration-img width="24px" height="24px" :src="item.image" alt="" />
|
||||
<div class="ml-[10px] flex-1">{{ item.name }}</div>
|
||||
<div>
|
||||
<icon name="el-icon-ArrowRight" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
import DecorationImg from '../../decoration-img.vue'
|
||||
import type options from './options'
|
||||
|
||||
type OptionsType = ReturnType<typeof options>
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: Object as PropType<OptionsType['content']>,
|
||||
default: () => ({})
|
||||
},
|
||||
styles: {
|
||||
type: Object as PropType<OptionsType['styles']>,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
const showList = computed(() => {
|
||||
return props.content.data?.filter((item: any) => item.is_show == '1') || []
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.my-service {
|
||||
margin: 10px 10px 0;
|
||||
background-color: #fff;
|
||||
border-radius: 7px;
|
||||
.title {
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,9 @@
|
||||
import attr from './attr.vue'
|
||||
import content from './content.vue'
|
||||
import options from './options'
|
||||
|
||||
export default {
|
||||
attr,
|
||||
content,
|
||||
options
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export default () => ({
|
||||
title: '我的服务',
|
||||
name: 'my-service',
|
||||
content: {
|
||||
style: 1,
|
||||
title: '我的服务',
|
||||
data: [
|
||||
{
|
||||
image: '',
|
||||
name: '导航名称',
|
||||
link: {}
|
||||
}
|
||||
]
|
||||
},
|
||||
styles: {}
|
||||
})
|
||||
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form label-width="70px">
|
||||
<el-card shadow="never" class="!border-none flex mt-2">
|
||||
<div class="flex items-end mb-4">
|
||||
<div class="text-base text-[#101010] font-medium">展示样式</div>
|
||||
</div>
|
||||
<el-radio-group v-model="contentData.style">
|
||||
<el-radio :value="1">固定显示</el-radio>
|
||||
<el-radio :value="2">分页滑动</el-radio>
|
||||
</el-radio-group>
|
||||
<el-form-item label="每行数量" class="mt-4">
|
||||
<el-select v-model="contentData.per_line" style="width: 300px">
|
||||
<el-option
|
||||
v-for="item in 5"
|
||||
:key="item"
|
||||
:label="item + '个'"
|
||||
:value="item"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="显示行数">
|
||||
<el-select v-model="contentData.show_line" style="width: 300px">
|
||||
<el-option
|
||||
v-for="item in 2"
|
||||
:key="item"
|
||||
:label="item + '行'"
|
||||
:value="item"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
<el-card shadow="never" class="!border-none flex mt-2">
|
||||
<div class="flex items-end">
|
||||
<div class="text-base text-[#101010] font-medium">菜单设置</div>
|
||||
<div class="text-xs text-tx-secondary ml-2">建议图片尺寸:100px*100px</div>
|
||||
</div>
|
||||
<div class="flex-1 mt-4">
|
||||
<AddNav v-model="contentData.data" />
|
||||
</div>
|
||||
</el-card>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
import AddNav from '../../add-nav.vue'
|
||||
import type options from './options'
|
||||
|
||||
type OptionsType = ReturnType<typeof options>
|
||||
const emits = defineEmits<(event: 'update:content', data: OptionsType['content']) => void>()
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: Object as PropType<OptionsType['content']>,
|
||||
default: () => ({})
|
||||
},
|
||||
styles: {
|
||||
type: Object as PropType<OptionsType['styles']>,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
const contentData = computed({
|
||||
get: () => props.content,
|
||||
set: (newValue) => {
|
||||
emits('update:content', newValue)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div class="nav bg-white pt-[15px] pb-[8px]">
|
||||
<div
|
||||
class="grid grid-rows-auto gap-y-3 w-full"
|
||||
:style="{ 'grid-template-columns': `repeat(${content.per_line}, 1fr)` }"
|
||||
>
|
||||
<div v-for="(item, index) in showList" :key="index" class="flex flex-col items-center">
|
||||
<decoration-img width="41px" height="41px" :src="item.image" alt="" />
|
||||
<div class="mt-[7px]">{{ item.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
import DecorationImg from '../../decoration-img.vue'
|
||||
import type options from './options'
|
||||
|
||||
type OptionsType = ReturnType<typeof options>
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: Object as PropType<OptionsType['content']>,
|
||||
default: () => ({})
|
||||
},
|
||||
styles: {
|
||||
type: Object as PropType<OptionsType['styles']>,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
const showList = computed(() => {
|
||||
const data = props.content.data?.filter((item: any) => item.is_show == '1') || []
|
||||
return data.slice(0, props.content.show_line * props.content.per_line)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,9 @@
|
||||
import attr from './attr.vue'
|
||||
import content from './content.vue'
|
||||
import options from './options'
|
||||
|
||||
export default {
|
||||
attr,
|
||||
content,
|
||||
options
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
export default () => ({
|
||||
title: '导航菜单',
|
||||
name: 'nav',
|
||||
content: {
|
||||
enabled: 1,
|
||||
style: 1, // 展示样式1=固定显示,2=分页显示
|
||||
per_line: 5, // 每行显示数量
|
||||
show_line: 2, // 显示行数
|
||||
data: [
|
||||
{
|
||||
image: '',
|
||||
name: '导航名称',
|
||||
link: {}
|
||||
}
|
||||
]
|
||||
},
|
||||
styles: {}
|
||||
})
|
||||
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
import type options from './options'
|
||||
|
||||
type OptionsType = ReturnType<typeof options>
|
||||
defineProps({
|
||||
content: {
|
||||
type: Object as PropType<OptionsType['content']>,
|
||||
default: () => ({})
|
||||
},
|
||||
styles: {
|
||||
type: Object as PropType<OptionsType['styles']>,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div class="news">
|
||||
<div class="flex items-center news-title mx-[10px] my-[15px] text-[17px] font-medium">
|
||||
最新资讯
|
||||
</div>
|
||||
<div
|
||||
v-for="item in newsList"
|
||||
:key="item.id"
|
||||
class="news-card flex bg-white px-[10px] py-[16px] text-[#333] border-[#f2f2f2] border-b"
|
||||
>
|
||||
<div class="mr-[10px]" v-if="item.image">
|
||||
<img :src="item.image" class="w-[120px] h-[90px] object-contain" />
|
||||
</div>
|
||||
<div class="flex flex-col justify-between flex-1">
|
||||
<div class="text-[15px] font-medium line-clamp-2">{{ item.title }}</div>
|
||||
<div class="line-clamp-1 text-sm mt-[8px]">
|
||||
{{ item.desc }}
|
||||
</div>
|
||||
|
||||
<div class="text-[#999] text-xs w-full flex justify-between mt-[8px]">
|
||||
<div>{{ item.create_time }}</div>
|
||||
<div class="flex items-center">
|
||||
<icon name="el-icon-View" />
|
||||
<div class="ml-[5px]">{{ item.click }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
import { getDecorateArticle } from '@/api/decoration'
|
||||
|
||||
import type options from './options'
|
||||
|
||||
type OptionsType = ReturnType<typeof options>
|
||||
defineProps({
|
||||
content: {
|
||||
type: Object as PropType<OptionsType['content']>,
|
||||
default: () => ({})
|
||||
},
|
||||
styles: {
|
||||
type: Object as PropType<OptionsType['styles']>,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
const newsList = ref<any[]>([])
|
||||
const getData = async () => {
|
||||
const data = await getDecorateArticle({
|
||||
limit: 10
|
||||
})
|
||||
newsList.value = data
|
||||
}
|
||||
getData()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.news {
|
||||
.news-title {
|
||||
&::before {
|
||||
content: '';
|
||||
width: 4px;
|
||||
height: 17px;
|
||||
display: block;
|
||||
margin-right: 5px;
|
||||
background: #4173ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,9 @@
|
||||
import attr from './attr.vue'
|
||||
import content from './content.vue'
|
||||
import options from './options'
|
||||
|
||||
export default {
|
||||
attr,
|
||||
content,
|
||||
options
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export default () => ({
|
||||
title: '资讯',
|
||||
name: 'news',
|
||||
disabled: 1,
|
||||
content: {},
|
||||
styles: {}
|
||||
})
|
||||
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<el-form ref="form" label-width="80px" size="large">
|
||||
<el-card shadow="never" class="!border-none flex mt-2">
|
||||
<el-form-item label="页面标题">
|
||||
<el-radio-group v-model="contentData.title_type">
|
||||
<el-radio value="1">文字</el-radio>
|
||||
<el-radio value="2">图片</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="content.title_type == 1">
|
||||
<el-input
|
||||
v-model="contentData.title"
|
||||
maxlength="8"
|
||||
show-word-limit
|
||||
class="w-[300px]"
|
||||
placeholder="请输入页面标题"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="content.title_type == 2">
|
||||
<material-picker v-model="contentData.title_img" :limit="1" size="100px" />
|
||||
<div class="form-tips">建议图片尺寸:300px*40px</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="文字颜色" v-if="content.title_type == 1">
|
||||
<el-radio-group v-model="contentData.text_color">
|
||||
<el-radio value="1">白色</el-radio>
|
||||
<el-radio value="2">黑色</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="页面背景">
|
||||
<el-radio-group v-model="contentData.bg_type">
|
||||
<el-radio value="1">背景颜色</el-radio>
|
||||
<el-radio value="2">背景图片</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="content.bg_type == 1">
|
||||
<color-picker v-model="contentData.bg_color" reset-color="#F5F5F5" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="content.bg_type == 2">
|
||||
<material-picker v-model="contentData.bg_image" :limit="1" size="100px" />
|
||||
<div class="form-tips">建议图片尺寸:750px*高度不限</div>
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
</el-form>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
import type options from './options'
|
||||
|
||||
type OptionsType = ReturnType<typeof options>
|
||||
const emits = defineEmits<(event: 'update:content', data: OptionsType['content']) => void>()
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: Object as PropType<OptionsType['content']>,
|
||||
default: () => ({})
|
||||
},
|
||||
styles: {
|
||||
type: Object as PropType<OptionsType['styles']>,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
const contentData = computed({
|
||||
get: () => props.content,
|
||||
set: (newValue) => {
|
||||
emits('update:content', newValue)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,6 @@
|
||||
<template>
|
||||
<div class="page-mate"></div>
|
||||
</template>
|
||||
<script lang="ts" setup></script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,9 @@
|
||||
import attr from './attr.vue'
|
||||
import content from './content.vue'
|
||||
import options from './options'
|
||||
|
||||
export default {
|
||||
attr,
|
||||
content,
|
||||
options
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export default () => ({
|
||||
title: '页面设置',
|
||||
name: 'page-meta',
|
||||
content: {
|
||||
title_type: 1,
|
||||
title: '',
|
||||
title_img: '',
|
||||
bg_type: 1,
|
||||
bg_color: '',
|
||||
bg_image: '',
|
||||
text_color: ''
|
||||
},
|
||||
styles: {}
|
||||
})
|
||||
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<popup ref="popupRef" title="轮播图设置" :async="true" width="980px" @confirm="handleSubmit">
|
||||
<el-alert title="最多可添加10张,建议图片尺寸750px*440px" type="warning" />
|
||||
|
||||
<el-scrollbar height="400px" class="mt-4">
|
||||
<div class="flex flex-wrap p-4">
|
||||
<div v-for="(item, index) in content.data" :key="index" class="w-[400px] mr-4 mb-4">
|
||||
<del-wrap :key="index" @close="handleDelete(index)" class="w-full">
|
||||
<div class="bg-fill-light w-full p-4">
|
||||
<div class="flex items-center">
|
||||
<material-picker
|
||||
width="122px"
|
||||
height="122px"
|
||||
v-model="item.image"
|
||||
upload-class="bg-body"
|
||||
exclude-domain
|
||||
>
|
||||
<template #upload>
|
||||
<div
|
||||
class="w-[122px] h-[122px] flex justify-center items-center"
|
||||
>
|
||||
轮播图
|
||||
</div>
|
||||
</template>
|
||||
</material-picker>
|
||||
<link-picker v-model="item.link" />
|
||||
</div>
|
||||
</div>
|
||||
</del-wrap>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 ml-4">
|
||||
<el-button link type="primary" @click="handleAdd">+ 添加轮播图</el-button>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</popup>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
import Popup from '@/components/popup/index.vue'
|
||||
import feedback from '@/utils/feedback'
|
||||
|
||||
import type options from './options'
|
||||
|
||||
type OptionsType = ReturnType<typeof options>
|
||||
const emits = defineEmits<(event: 'update:content', data: OptionsType['content']) => void>()
|
||||
const popupRef = shallowRef<InstanceType<typeof Popup>>()
|
||||
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: Object as PropType<OptionsType['content']>,
|
||||
default: () => ({})
|
||||
},
|
||||
styles: {
|
||||
type: Object as PropType<OptionsType['styles']>,
|
||||
default: () => ({})
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '170px'
|
||||
}
|
||||
})
|
||||
|
||||
const open = () => {
|
||||
popupRef.value?.open()
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
popupRef.value?.close()
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
if (props.content.data?.length < 10) {
|
||||
const content = cloneDeep(props.content)
|
||||
content.data.push({
|
||||
image: '',
|
||||
name: '',
|
||||
link: {}
|
||||
})
|
||||
emits('update:content', content)
|
||||
} else {
|
||||
feedback.msgError(`最多添加${10}张图片`)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = (index: number) => {
|
||||
if (props.content.data?.length <= 1) {
|
||||
return feedback.msgError('最少保留一个轮播图')
|
||||
}
|
||||
const content = cloneDeep(props.content)
|
||||
content.data.splice(index, 1)
|
||||
emits('update:content', content)
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,7 @@
|
||||
import content from './content.vue'
|
||||
import options from './options'
|
||||
|
||||
export default {
|
||||
content,
|
||||
options
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export default () => ({
|
||||
title: '首页轮播图',
|
||||
name: 'pc-banner',
|
||||
content: {
|
||||
enabled: 1,
|
||||
data: [
|
||||
{
|
||||
image: '',
|
||||
name: '',
|
||||
link: {}
|
||||
}
|
||||
]
|
||||
},
|
||||
styles: {}
|
||||
})
|
||||
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
import type options from './options'
|
||||
|
||||
type OptionsType = ReturnType<typeof options>
|
||||
defineProps({
|
||||
content: {
|
||||
type: Object as PropType<OptionsType['content']>,
|
||||
default: () => ({})
|
||||
},
|
||||
styles: {
|
||||
type: Object as PropType<OptionsType['styles']>,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<div class="search">
|
||||
<div class="search-con flex items-center px-[15px]">
|
||||
<icon name="el-icon-Search" :size="17" />
|
||||
<span class="ml-[5px]">请输入关键词搜索</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup></script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.search {
|
||||
background-color: #fff;
|
||||
padding: 7px 12px;
|
||||
.search-con {
|
||||
height: 100%;
|
||||
height: 36px;
|
||||
border-radius: 36px;
|
||||
background: #f4f4f4;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,9 @@
|
||||
import attr from './attr.vue'
|
||||
import content from './content.vue'
|
||||
import options from './options'
|
||||
|
||||
export default {
|
||||
attr,
|
||||
content,
|
||||
options
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export default () => ({
|
||||
title: '搜索',
|
||||
name: 'search',
|
||||
disabled: 1,
|
||||
content: {},
|
||||
styles: {}
|
||||
})
|
||||
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form label-width="70px">
|
||||
<el-card shadow="never" class="!border-none flex mt-2">
|
||||
<div class="flex items-end mb-4">
|
||||
<div class="text-base text-[#101010] font-medium">菜单</div>
|
||||
<div class="text-xs text-tx-secondary ml-2">
|
||||
最多添加5张,建议图片尺寸:750px*200px
|
||||
</div>
|
||||
</div>
|
||||
<draggable
|
||||
class="draggable"
|
||||
v-model="contentData.data"
|
||||
animation="300"
|
||||
handle=".drag-move"
|
||||
item-key="index"
|
||||
>
|
||||
<template v-slot:item="{ element: item, index }">
|
||||
<del-wrap :key="index" @close="handleDelete(index)" class="w-[467px]">
|
||||
<div class="bg-fill-light flex items-center w-full p-4 mt-4">
|
||||
<material-picker
|
||||
v-model="item.image"
|
||||
upload-class="bg-body"
|
||||
exclude-domain
|
||||
/>
|
||||
<div class="ml-3 flex-1">
|
||||
<el-form-item label="图片名称">
|
||||
<el-input v-model="item.name" placeholder="请输入名称" />
|
||||
</el-form-item>
|
||||
<el-form-item class="mt-[18px]" label="图片链接">
|
||||
<link-picker v-model="item.link" />
|
||||
</el-form-item>
|
||||
<el-form-item label="是否显示" class="mt-[18px]">
|
||||
<div class="flex-1 flex items-center">
|
||||
<el-switch
|
||||
v-model="item.is_show"
|
||||
active-value="1"
|
||||
inactive-value="0"
|
||||
/>
|
||||
<div class="drag-move cursor-move ml-auto">
|
||||
<icon name="el-icon-Rank" size="18" />
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</del-wrap>
|
||||
</template>
|
||||
</draggable>
|
||||
<div class="mt-4" v-if="content.data?.length < limit">
|
||||
<el-button class="w-full" type="primary" @click="handleAdd">添加图片</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import type { PropType } from 'vue'
|
||||
import Draggable from 'vuedraggable'
|
||||
|
||||
import feedback from '@/utils/feedback'
|
||||
|
||||
import type options from './options'
|
||||
|
||||
type OptionsType = ReturnType<typeof options>
|
||||
const emits = defineEmits<(event: 'update:content', data: OptionsType['content']) => void>()
|
||||
|
||||
const limit = 5
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: Object as PropType<OptionsType['content']>,
|
||||
default: () => ({})
|
||||
},
|
||||
styles: {
|
||||
type: Object as PropType<OptionsType['styles']>,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
const contentData = computed({
|
||||
get: () => props.content,
|
||||
set: (newValue) => {
|
||||
emits('update:content', newValue)
|
||||
}
|
||||
})
|
||||
|
||||
const handleAdd = () => {
|
||||
if (props.content.data?.length < limit) {
|
||||
const content = cloneDeep(props.content)
|
||||
content.data.push({
|
||||
is_show: '1',
|
||||
image: '',
|
||||
name: '',
|
||||
link: {}
|
||||
})
|
||||
emits('update:content', content)
|
||||
} else {
|
||||
feedback.msgError(`最多添加${limit}张图片`)
|
||||
}
|
||||
}
|
||||
const handleDelete = (index: number) => {
|
||||
if (props.content.data?.length <= 1) {
|
||||
return feedback.msgError('最少保留一张图片')
|
||||
}
|
||||
const content = cloneDeep(props.content)
|
||||
content.data.splice(index, 1)
|
||||
emits('update:content', content)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div class="banner mx-[10px] mt-[10px]">
|
||||
<div class="banner-image">
|
||||
<decoration-img width="100%" height="100px" :src="getImage" fit="contain" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
import DecorationImg from '../../decoration-img.vue'
|
||||
import type options from './options'
|
||||
|
||||
type OptionsType = ReturnType<typeof options>
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: Object as PropType<OptionsType['content']>,
|
||||
default: () => ({})
|
||||
},
|
||||
styles: {
|
||||
type: Object as PropType<OptionsType['styles']>,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
const showList = computed(() => {
|
||||
return props.content.data?.filter((item: any) => item.is_show == '1') || []
|
||||
})
|
||||
const getImage = computed(() => {
|
||||
if (Array.isArray(showList.value)) {
|
||||
return showList.value[0] ? showList.value[0].image : ''
|
||||
}
|
||||
return ''
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,9 @@
|
||||
import attr from './attr.vue'
|
||||
import content from './content.vue'
|
||||
import options from './options'
|
||||
|
||||
export default {
|
||||
attr,
|
||||
content,
|
||||
options
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export default () => ({
|
||||
title: '个人中心广告图',
|
||||
name: 'user-banner',
|
||||
content: {
|
||||
enabled: 1,
|
||||
data: [
|
||||
{
|
||||
is_show: '1',
|
||||
image: '',
|
||||
name: '',
|
||||
link: {}
|
||||
}
|
||||
]
|
||||
},
|
||||
styles: {}
|
||||
})
|
||||
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
import type options from './options'
|
||||
|
||||
type OptionsType = ReturnType<typeof options>
|
||||
defineProps({
|
||||
content: {
|
||||
type: Object as PropType<OptionsType['content']>,
|
||||
default: () => ({})
|
||||
},
|
||||
styles: {
|
||||
type: Object as PropType<OptionsType['styles']>,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div class="user-info flex items-center px-[25px]">
|
||||
<img src="./images/default_avatar.png" class="w-[60px] h-[60px]" alt="" />
|
||||
<div class="text-white text-[18px] ml-[10px]">未登录</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup></script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.user-info {
|
||||
background: url(./images/my_topbg.png);
|
||||
height: 115px;
|
||||
background-position: bottom;
|
||||
background-size: 100% auto;
|
||||
}
|
||||
</style>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 6.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 139 KiB |
@@ -0,0 +1,9 @@
|
||||
import attr from './attr.vue'
|
||||
import content from './content.vue'
|
||||
import options from './options'
|
||||
|
||||
export default {
|
||||
attr,
|
||||
content,
|
||||
options
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export default () => ({
|
||||
title: '用户信息',
|
||||
name: 'user-info',
|
||||
disabled: 1,
|
||||
content: {},
|
||||
styles: {}
|
||||
})
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 516 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user