feat: 本地 Java 发票 OCR + 策划方案多角色开放 + H5 相机脱敏修复

- OCR: 进程内 PaddleOCR ONNX Runtime 识别替代远程 Python 微服务
  (新增 ocr/core/service/config 引擎, 删 OcrClient/OcrConfig, PDF 文本层优先 + QR 快路径)
- 投稿: 项目设计投稿 → 项目策划方案, doctor/executor/sponsor 三角色开放
  (BizProjectPlan 按非 admin/manager 隔离, 复用 Submissions.vue, 设计文件改 OssFileUploader)
- H5: <video> 去掉 autoplay + safePlay 重试, 穿透 uni-app wrapper 找原生 video
- 脱敏: 签到表高斯模糊由横向条带改为全高竖条带 (20%~50% 宽度)
- 域名: ringdoctor.com → risingdoctor.com
This commit is contained in:
郭庆泰
2026-08-24 19:23:53 +08:00
parent 904e28710f
commit 0c882ae846
36 changed files with 2851 additions and 296 deletions
@@ -1,7 +1,7 @@
<template>
<div v-loading="loading" class="page-card doctor-submission-detail">
<!-- 面包屑 (标准: 首页 / 分类 / 当前页) -->
<div class="breadcrumb">首页 / 我的项目设计投稿 / 投稿详情</div>
<div class="breadcrumb">首页 / 我的项目策划方案 / 策划方案详情</div>
<!-- 表单 (只读, el-input readonly 展示) -->
<el-form label-width="120px" class="readonly-form">
@@ -68,6 +68,8 @@ import request from '@/utils/request'
const route = useRoute()
const router = useRouter()
// 投稿模块路径前缀: 按当前角色 (doctor/executor/sponsor) 拼, 避免串到别的角色侧栏
const base = computed(() => `/${route.meta.role || 'doctor'}`)
const loading = ref(false)
const detail = ref({})
@@ -87,7 +89,7 @@ const display = computed(() => ({
}))
function goBack() {
router.push({ path: '/doctor/submissions', query: { _t: Date.now() } })
router.push({ path: `${base.value}/submissions`, query: { _t: Date.now() } })
}
async function load() {
+14 -54
View File
@@ -1,7 +1,7 @@
<template>
<div v-loading="loadingDetail" class="page-card doctor-submission-new">
<div class="breadcrumb">
<span>首页 / 我的项目设计投稿 / {{ isEdit ? '修改项目设计投稿' : '新建项目设计投稿' }}</span>
<span>首页 / 我的项目策划方案 / {{ isEdit ? '修改项目策划方案' : '新建项目策划方案' }}</span>
</div>
<el-form :model="form" :rules="rules" ref="formRef" label-width="120px" class="submission-form">
@@ -41,20 +41,14 @@
<el-row :gutter="12">
<el-col :span="24">
<el-form-item label="设计文件" prop="designFileUrl">
<el-upload
drag
action="#"
accept=".pdf,.png,.jpg,.jpeg"
:show-file-list="true"
:file-list="designFileList"
:before-upload="beforeDesignUpload"
:http-request="uploadDesign"
:on-remove="onDesignRemove"
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">将设计文件拖到此处,<em>点击上传</em></div>
<div class="el-upload__tip" slot="tip">支持 PDF / 图片, 单文件 20MB</div>
</el-upload>
<OssFileUploader
v-model="form.designFileUrl"
dir="ry8080/submission/design/"
placeholder="点击上传设计文件"
hint="支持 PDF / 图片, 单文件 ≤ 20MB"
:max-size="20"
block
/>
</el-form-item>
</el-col>
</el-row>
@@ -76,24 +70,24 @@
</template>
<script setup>
import { reactive, ref, onMounted } from 'vue'
import { reactive, ref, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useRoute, useRouter } from 'vue-router'
import { useUserStore } from '@/store/user'
import request from '@/utils/request'
import { bizAdd, bizUpdate } from '@/api/public'
import { uploadToOss } from '@/utils/oss'
import DictSelect from '@/components/DictSelect.vue'
import OssFileUploader from '@/components/OssFileUploader.vue'
const router = useRouter()
const route = useRoute()
const userStore = useUserStore()
// 投稿模块路径前缀: 按当前角色 (doctor/executor/sponsor) 拼, 避免串到别的角色侧栏
const base = computed(() => `/${route.meta.role || 'doctor'}`)
const formRef = ref(null)
const saving = ref(false)
const loadingDetail = ref(false)
const designFileList = ref([])
const designUploading = ref(false)
const isEdit = !!route.params.planId
// 项目方向 options (复用 manager 侧接口)
@@ -124,36 +118,6 @@ const rules = {
designFileUrl: [{ required: true, message: '请上传设计文件', trigger: 'change' }]
}
function beforeDesignUpload(file) {
const max = 20 * 1024 * 1024
if (file.size > max) { ElMessage.error('设计文件不能超过 20MB'); return false }
const accept = ['.pdf', '.png', '.jpg', '.jpeg']
const ext = '.' + (file.name.split('.').pop() || '').toLowerCase()
if (!accept.includes(ext)) { ElMessage.error('仅支持 PDF / 图片格式'); return false }
return true
}
async function uploadDesign(opts) {
const file = opts.file
designUploading.value = true
try {
const url = await uploadToOss(file, 'ry8080/submission/design/')
form.designFileUrl = url
designFileList.value = [{ name: file.name, url }]
ElMessage.success('设计文件上传成功')
} catch (e) {
ElMessage.error('上传失败: ' + (e?.message || e))
designFileList.value = []
} finally {
designUploading.value = false
}
}
function onDesignRemove() {
form.designFileUrl = ''
designFileList.value = []
}
async function loadDetail() {
const planId = route.params.planId
if (!planId) return
@@ -170,10 +134,6 @@ async function loadDetail() {
form.designFileUrl = d.designFileUrl || ''
form.status = d.status && String(d.status).trim() ? String(d.status) : '0'
form.remark = d.remark || ''
if (d.designFileUrl) {
const name = d.designFileUrl.split('/').pop() || `${d.planName || '设计文件'}.pdf`
designFileList.value = [{ name, url: d.designFileUrl }]
}
} else {
ElMessage.error(res?.msg || '加载失败')
goBack()
@@ -188,7 +148,7 @@ async function loadDetail() {
}
function goBack() {
router.push({ path: '/doctor/submissions', query: { _t: Date.now() } })
router.push({ path: `${base.value}/submissions`, query: { _t: Date.now() } })
}
function confirmCancel() {
+9 -6
View File
@@ -1,6 +1,6 @@
<template>
<div class="page-card">
<div class="breadcrumb">首页 / 我的项目设计投稿</div>
<div class="breadcrumb">首页 / 我的项目策划方案</div>
<el-form inline :model="q" class="filter-form">
<el-form-item label="策划方案名称"><el-input v-model="q.planName" placeholder="输入策划方案名称" clearable style="width: 200px" /></el-form-item>
@@ -81,15 +81,18 @@
</template>
<script setup>
import { reactive, ref, onMounted } from 'vue'
import { reactive, ref, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useRouter } from 'vue-router'
import { useRouter, useRoute } from 'vue-router'
import request from '@/utils/request'
import { bizList, bizUpdate } from '@/api/public'
import DictSelect from '@/components/DictSelect.vue'
import AuditStatusTag from '@/components/AuditStatusTag.vue'
const router = useRouter()
const route = useRoute()
// 投稿模块路径前缀: 按当前角色 (doctor/executor/sponsor) 拼, 避免串到别的角色侧栏
const base = computed(() => `/${route.meta.role || 'doctor'}`)
const q = reactive({
planName: '',
@@ -146,13 +149,13 @@ function canSubmit(row) {
function onSelectionChange(arr) { selected.value = arr }
function onCreate() {
router.push({ path: '/doctor/submission/new' })
router.push({ path: `${base.value}/submission/new` })
}
function onView(row) {
router.push({ path: `/doctor/submission/detail/${row.planId}` })
router.push({ path: `${base.value}/submission/detail/${row.planId}` })
}
function onEdit(row) {
router.push({ path: `/doctor/submission/edit/${row.planId}` })
router.push({ path: `${base.value}/submission/edit/${row.planId}` })
}
async function onSubmit(row) {
+9 -2
View File
@@ -48,7 +48,7 @@
</p>
</div>
<div class="hero-cta">
<span class="cta-btn" @click="onSubmit">项目提案</span>
<span v-if="canPropose" class="cta-btn" @click="onSubmit">项目提案</span>
</div>
</section>
@@ -169,6 +169,11 @@ function openPlan(id) {
const topNavClass = computed(() => isScrolled.value ? 'is-scrolled' : '')
const loggedIn = computed(() => !!userStore.token)
const userName = computed(() => userStore.user?.userName || '用户')
// 投稿角色: admin/manager 不开放投稿 → 隐藏「项目提案」按钮; 未登录 user 为 null → 显示(点击引导登录)
const canPropose = computed(() => {
const r = userStore.user?.role || ''
return r !== 'admin' && r !== 'manager'
})
const planSvg = `<svg viewBox="0 150 1200 370" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="2025-2030年项目规划">
<defs>
@@ -257,7 +262,9 @@ function onSubmit() {
router.push('/login')
return
}
router.push('/publicity')
const role = userStore.user?.role
const map = { doctor: '/doctor/submissions', executor: '/executor/submissions', sponsor: '/sponsor/submissions' }
router.push(map[role] || '/doctor/submissions')
}
</script>