Files
a_long_vue/src/views/miniapp/XieyiManage.vue
2026-06-27 02:27:50 +08:00

340 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>