fix: 将 admin 改为普通目录

This commit is contained in:
hajimi
2026-07-31 16:39:16 +08:00
parent e7f9328611
commit fdf8022662
592 changed files with 41318 additions and 1 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

+123
View File
@@ -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>
+67
View File
@@ -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>
+63
View File
@@ -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>
+62
View File
@@ -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>
+69
View File
@@ -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>
+60
View File
@@ -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>
+92
View File
@@ -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>
+59
View File
@@ -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>
+53
View File
@@ -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>
+216
View File
@@ -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="按行书写更新内容,每行一个&#10;例如:&#10;1. 修复登录bug&#10;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>
+163
View File
@@ -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>
+95
View File
@@ -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>
+89
View File
@@ -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>
+146
View File
@@ -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>
+171
View File
@@ -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>
+65
View File
@@ -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>
+76
View File
@@ -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>
+199
View File
@@ -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*高400pxjpgjpegpng格式</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>
+217
View File
@@ -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*高400pxjpgjpegpng格式</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>
+47
View File
@@ -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>
+148
View File
@@ -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>
+210
View File
@@ -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>
+171
View File
@@ -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>
+159
View File
@@ -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>
+169
View File
@@ -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>
+141
View File
@@ -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>
+127
View File
@@ -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