feat: 用户协议管理页与海报资源组配置

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-06-27 02:27:50 +08:00
parent d142c82d6e
commit bf62e2e5dc
4 changed files with 346 additions and 0 deletions

View File

@@ -228,6 +228,11 @@ const routes = [
component: () => import('@/views/miniapp/MiniappConfig.vue'),
meta: { title: '微信与页面资源配置' }
},
{
path: 'miniapp/xieyi-manage',
component: () => import('@/views/miniapp/XieyiManage.vue'),
meta: { title: '用户协议管理' }
},
// ========== 🔥 新增:客服会话管理 ==========

View File

@@ -137,6 +137,7 @@
<el-menu-item index="/miniapp/popup-notice">弹窗公告管理</el-menu-item>
<el-menu-item index="/miniapp/script-config">自动弹窗及话术配置</el-menu-item>
<el-menu-item index="/miniapp/sys-config">微信与页面资源配置</el-menu-item>
<el-menu-item index="/miniapp/xieyi-manage">用户协议管理</el-menu-item>
</el-sub-menu>

View File

@@ -67,6 +67,7 @@
<el-option label="排行榜" value="paihangbang" />
<el-option label="商家端" value="shangjiaduan" />
<el-option label="管事端" value="guanshiduan" />
<el-option label="推广海报" value="haibao" />
</el-select>
<el-button type="primary" :loading="assetSaving" @click="saveAssets">保存路径</el-button>
<el-button @click="loadAssets">刷新</el-button>

View File

@@ -0,0 +1,339 @@
<template>
<div class="xieyi-manager">
<div v-if="!hasPermission" class="no-permission">
<div class="no-permission-icon">🔒</div>
<div>您无权访问此页面</div>
</div>
<div v-else>
<div class="header-bar">
<h2>用户协议管理</h2>
<el-text type="info">支持按身份创建文字/图片协议用户手写签署后可在下方查询记录</el-text>
</div>
<div class="toolbar">
<el-select v-model="filterShenfen" placeholder="筛选身份" clearable style="width: 160px" @change="loadList">
<el-option v-for="s in shenfenChoices" :key="s.key" :label="s.label" :value="s.key" />
</el-select>
<el-button type="primary" @click="openEdit()">+ 新建协议</el-button>
<el-button @click="loadList">刷新</el-button>
</div>
<el-table :data="items" border stripe v-loading="loading">
<el-table-column prop="title" label="后台标题" min-width="160" />
<el-table-column prop="shenfen_label" label="适用身份" width="100" />
<el-table-column label="模式" width="90">
<template #default="{ row }">{{ row.mode === 1 ? '图片' : '文字' }}</template>
</el-table-column>
<el-table-column prop="version" label="版本" width="70" />
<el-table-column label="状态" width="80">
<template #default="{ row }">
<el-tag :type="row.is_active ? 'success' : 'info'" size="small">
{{ row.is_active ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
<el-button link type="danger" @click="removeItem(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-divider>签署记录查询</el-divider>
<div class="sign-toolbar">
<el-input v-model="signYonghuid" placeholder="用户ID" style="width: 160px" clearable />
<el-button type="primary" @click="loadSignRecords">查询签署</el-button>
</div>
<el-table :data="signRecords" border size="small" v-loading="signLoading">
<el-table-column prop="yonghuid" label="用户ID" width="100" />
<el-table-column prop="xieyi_title" label="协议" min-width="140" />
<el-table-column prop="xieyi_version" label="版本" width="70" />
<el-table-column prop="shenfen" label="签署身份" width="90" />
<el-table-column prop="signed_at" label="签署时间" width="170" />
<el-table-column label="签名图" width="100">
<template #default="{ row }">
<el-image v-if="row.signature_path" :src="fullUrl(row.signature_path)" :preview-src-list="[fullUrl(row.signature_path)]" class="sign-thumb" />
</template>
</el-table-column>
</el-table>
</div>
<el-dialog v-model="dialogVisible" :title="form.id ? '编辑协议' : '新建协议'" width="720px" destroy-on-close>
<el-form :model="form" label-width="120px">
<el-form-item label="后台标题" required>
<el-input v-model="form.title" />
</el-form-item>
<el-form-item label="适用身份" required>
<el-select v-model="form.shenfen">
<el-option v-for="s in shenfenChoices" :key="s.key" :label="s.label" :value="s.key" />
</el-select>
</el-form-item>
<el-form-item label="展示模式">
<el-radio-group v-model="form.mode">
<el-radio :value="2">文字模式</el-radio>
<el-radio :value="1">纯图片模式</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="前端标题">
<el-input v-model="form.content_title" placeholder="可选" />
</el-form-item>
<el-form-item v-if="form.mode === 2" label="文字内容">
<el-input v-model="form.content_text" type="textarea" :rows="10" placeholder="支持换行" />
</el-form-item>
<el-form-item v-if="form.mode === 1 && form.id" label="协议图片">
<div class="img-list">
<div v-for="img in form.images" :key="img.id" class="img-item">
<el-image :src="fullUrl(img.image_path)" fit="cover" class="preview" />
<el-button link type="danger" @click="deleteImage(img)">删除</el-button>
</div>
<el-upload :show-file-list="false" accept="image/*" :http-request="uploadImage">
<el-button type="primary" link>+ 上传图片</el-button>
</el-upload>
</div>
</el-form-item>
<el-form-item label="排序">
<el-input-number v-model="form.sort_order" :min="0" />
</el-form-item>
<el-form-item label="启用">
<el-switch v-model="form.is_active" />
</el-form-item>
<el-form-item v-if="form.id" label="升级版本">
<el-switch v-model="form.bump_version" />
<el-text type="info" size="small">修改内容后开启用户需重新签署</el-text>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitting" @click="saveForm">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import request from '@/utils/request'
const username = ref(localStorage.getItem('username') || '')
const hasPermission = ref(true)
const loading = ref(false)
const items = ref([])
const shenfenChoices = ref([])
const filterShenfen = ref('')
const dialogVisible = ref(false)
const submitting = ref(false)
const ossBaseUrl = ref(window.$ossURL || '')
const form = reactive({
id: null,
title: '',
shenfen: 'dashou',
mode: 2,
content_title: '',
content_text: '',
sort_order: 0,
is_active: true,
bump_version: false,
images: [],
})
const signYonghuid = ref('')
const signRecords = ref([])
const signLoading = ref(false)
const fullUrl = (path) => {
if (!path) return ''
if (path.startsWith('http')) return path
const base = ossBaseUrl.value.replace(/\/$/, '')
return `${base}/${String(path).replace(/^\//, '')}`
}
const loadList = async () => {
loading.value = true
try {
const res = await request.post('/houtai/hthqxy', {
username: username.value,
shenfen: filterShenfen.value || undefined,
})
if (res.code === 403) {
hasPermission.value = false
return
}
if (res.code === 0) {
items.value = res.data?.items || []
shenfenChoices.value = res.data?.shenfen_choices || []
} else {
ElMessage.error(res.msg || '加载失败')
}
} catch {
ElMessage.error('网络错误')
} finally {
loading.value = false
}
}
const openEdit = (row) => {
if (row) {
Object.assign(form, {
id: row.id,
title: row.title,
shenfen: row.shenfen,
mode: row.mode,
content_title: row.content_title || '',
content_text: row.content_text || '',
sort_order: row.sort_order || 0,
is_active: row.is_active,
bump_version: false,
images: row.images || [],
})
} else {
Object.assign(form, {
id: null,
title: '',
shenfen: 'dashou',
mode: 2,
content_title: '',
content_text: '',
sort_order: 0,
is_active: true,
bump_version: false,
images: [],
})
}
dialogVisible.value = true
}
const saveForm = async () => {
if (!form.title || !form.shenfen) {
ElMessage.warning('请填写标题和身份')
return
}
submitting.value = true
try {
const payload = {
username: username.value,
action: form.id ? 'update' : 'create',
id: form.id,
title: form.title,
shenfen: form.shenfen,
mode: form.mode,
content_title: form.content_title,
content_text: form.content_text,
sort_order: form.sort_order,
is_active: form.is_active,
bump_version: form.bump_version,
}
const res = await request.post('/houtai/htxgxy', payload)
if (res.code === 0) {
ElMessage.success(res.msg || '保存成功')
dialogVisible.value = false
loadList()
} else {
ElMessage.error(res.msg || '保存失败')
}
} catch {
ElMessage.error('网络错误')
} finally {
submitting.value = false
}
}
const removeItem = async (row) => {
try {
await ElMessageBox.confirm(`确定删除「${row.title}」?`, '确认')
const res = await request.post('/houtai/htxgxy', {
username: username.value,
action: 'delete',
id: row.id,
})
if (res.code === 0) {
ElMessage.success('已删除')
loadList()
} else {
ElMessage.error(res.msg || '删除失败')
}
} catch {}
}
const uploadImage = async ({ file }) => {
if (!form.id) {
ElMessage.warning('请先保存协议再上传图片')
return
}
const fd = new FormData()
fd.append('username', username.value)
fd.append('xieyi_id', form.id)
fd.append('image', file)
try {
const res = await request.post('/houtai/htxgytp', fd, {
headers: { 'Content-Type': 'multipart/form-data' },
})
if (res.code === 0) {
ElMessage.success('上传成功')
form.images.push({ id: res.data.id, image_path: res.data.image_path })
loadList()
} else {
ElMessage.error(res.msg || '上传失败')
}
} catch {
ElMessage.error('上传失败')
}
}
const deleteImage = async (img) => {
try {
const res = await request.post('/houtai/htscxytp', {
username: username.value,
image_id: img.id,
})
if (res.code === 0) {
form.images = form.images.filter((i) => i.id !== img.id)
ElMessage.success('已删除')
loadList()
} else {
ElMessage.error(res.msg || '删除失败')
}
} catch {
ElMessage.error('网络错误')
}
}
const loadSignRecords = async () => {
signLoading.value = true
try {
const res = await request.post('/houtai/hthqxyqs', {
username: username.value,
yonghuid: signYonghuid.value || undefined,
})
if (res.code === 0) {
signRecords.value = res.data?.items || []
} else {
ElMessage.error(res.msg || '查询失败')
}
} catch {
ElMessage.error('网络错误')
} finally {
signLoading.value = false
}
}
onMounted(() => {
loadList()
})
</script>
<style scoped>
.xieyi-manager { padding: 8px 4px; }
.header-bar { margin-bottom: 16px; }
.header-bar h2 { margin: 0 0 8px; color: #00f2ff; }
.toolbar, .sign-toolbar { display: flex; gap: 12px; margin-bottom: 16px; align-items: center; }
.no-permission { text-align: center; padding: 80px; color: rgba(255,255,255,0.6); }
.no-permission-icon { font-size: 48px; margin-bottom: 12px; }
.img-list { display: flex; flex-wrap: wrap; gap: 12px; }
.img-item { text-align: center; }
.preview { width: 120px; height: 120px; border-radius: 8px; }
.sign-thumb { width: 56px; height: 56px; border-radius: 4px; }
</style>