340 lines
11 KiB
Vue
340 lines
11 KiB
Vue
<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>
|