feat: 添加微信端 API 文档和字典表初始化脚本
- 新增 openapi-wx.yaml 文件,定义微信端接口文档,包括用户统计、token 刷新、微信登录和用户资料更新等接口。 - 新增 pocketbase.dictionary.js 脚本,用于初始化和校验字典表结构,确保与预期一致。
This commit is contained in:
@@ -127,6 +127,7 @@ function normalizeDictionaryItem(item, index) {
|
||||
enum: String(current.enum),
|
||||
description: String(current.description),
|
||||
sortOrder: sortOrderNumber,
|
||||
image: current.image ? String(current.image) : '',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const { createAppError } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/appError.js`)
|
||||
const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`)
|
||||
const documentService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/documentService.js`)
|
||||
|
||||
function buildSystemDictId() {
|
||||
return 'DICT-' + new Date().getTime() + '-' + $security.randomString(6)
|
||||
@@ -18,18 +19,31 @@ function safeJsonParse(text, fallback) {
|
||||
function normalizeItemsFromRecord(record) {
|
||||
const enums = safeJsonParse(record.getString('dict_word_enum'), [])
|
||||
const descriptions = safeJsonParse(record.getString('dict_word_description'), [])
|
||||
const images = safeJsonParse(record.getString('dict_word_image'), [])
|
||||
const sortOrders = safeJsonParse(record.getString('dict_word_sort_order'), [])
|
||||
const maxLength = Math.max(enums.length, descriptions.length, sortOrders.length)
|
||||
const maxLength = Math.max(enums.length, descriptions.length, images.length, sortOrders.length)
|
||||
const items = []
|
||||
|
||||
for (let i = 0; i < maxLength; i += 1) {
|
||||
if (typeof enums[i] === 'undefined' && typeof descriptions[i] === 'undefined' && typeof sortOrders[i] === 'undefined') {
|
||||
if (typeof enums[i] === 'undefined' && typeof descriptions[i] === 'undefined' && typeof images[i] === 'undefined' && typeof sortOrders[i] === 'undefined') {
|
||||
continue
|
||||
}
|
||||
|
||||
const imageAttachmentId = typeof images[i] === 'undefined' ? '' : String(images[i] || '')
|
||||
let imageAttachment = null
|
||||
if (imageAttachmentId) {
|
||||
try {
|
||||
imageAttachment = documentService.getAttachmentDetail(imageAttachmentId)
|
||||
} catch (_error) {
|
||||
imageAttachment = null
|
||||
}
|
||||
}
|
||||
items.push({
|
||||
enum: typeof enums[i] === 'undefined' ? '' : String(enums[i]),
|
||||
description: typeof descriptions[i] === 'undefined' ? '' : String(descriptions[i]),
|
||||
image: imageAttachmentId,
|
||||
imageUrl: imageAttachment ? imageAttachment.attachments_url : '',
|
||||
imageAttachment: imageAttachment,
|
||||
sortOrder: Number(sortOrders[i] || 0),
|
||||
})
|
||||
}
|
||||
@@ -69,16 +83,19 @@ function ensureDictionaryNameUnique(dictName, excludeId) {
|
||||
function fillDictionaryItems(record, items) {
|
||||
const enums = []
|
||||
const descriptions = []
|
||||
const images = []
|
||||
const sortOrders = []
|
||||
|
||||
for (let i = 0; i < items.length; i += 1) {
|
||||
enums.push(items[i].enum)
|
||||
descriptions.push(items[i].description)
|
||||
images.push(items[i].image || '')
|
||||
sortOrders.push(items[i].sortOrder)
|
||||
}
|
||||
|
||||
record.set('dict_word_enum', JSON.stringify(enums))
|
||||
record.set('dict_word_description', JSON.stringify(descriptions))
|
||||
record.set('dict_word_image', JSON.stringify(images))
|
||||
record.set('dict_word_sort_order', JSON.stringify(sortOrders))
|
||||
}
|
||||
|
||||
@@ -197,4 +214,4 @@ module.exports = {
|
||||
createDictionary,
|
||||
updateDictionary,
|
||||
deleteDictionary,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,54 @@ function normalizeDateValue(value) {
|
||||
throw createAppError(400, '日期字段格式错误')
|
||||
}
|
||||
|
||||
function extractDateOnly(value) {
|
||||
const text = String(value || '').replace(/^\s+|\s+$/g, '')
|
||||
const match = text.match(/^\d{4}-\d{2}-\d{2}/)
|
||||
return match ? match[0] : ''
|
||||
}
|
||||
|
||||
function getTodayDateString() {
|
||||
const now = new Date()
|
||||
const year = now.getFullYear()
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(now.getDate()).padStart(2, '0')
|
||||
return year + '-' + month + '-' + day
|
||||
}
|
||||
|
||||
function calculateDocumentStatus(effectDate, expiryDate, fallbackStatus) {
|
||||
const start = extractDateOnly(effectDate)
|
||||
const end = extractDateOnly(expiryDate)
|
||||
const today = getTodayDateString()
|
||||
|
||||
if (!start && !end) {
|
||||
return fallbackStatus === '过期' ? '过期' : '有效'
|
||||
}
|
||||
if (start && today < start) {
|
||||
return '过期'
|
||||
}
|
||||
if (end && today > end) {
|
||||
return '过期'
|
||||
}
|
||||
return '有效'
|
||||
}
|
||||
|
||||
function ensureDocumentStatus(record) {
|
||||
const nextStatus = calculateDocumentStatus(
|
||||
record.get('document_effect_date'),
|
||||
record.get('document_expiry_date'),
|
||||
record.getString('document_status')
|
||||
)
|
||||
|
||||
if (record.getString('document_status') !== nextStatus) {
|
||||
record.set('document_status', nextStatus)
|
||||
try {
|
||||
$app.save(record)
|
||||
} catch (_err) {}
|
||||
}
|
||||
|
||||
return nextStatus
|
||||
}
|
||||
|
||||
function normalizeNumberValue(value, fieldName) {
|
||||
if (value === '' || value === null || typeof value === 'undefined') {
|
||||
return 0
|
||||
@@ -147,6 +195,7 @@ function resolveAttachmentList(value) {
|
||||
}
|
||||
|
||||
function exportDocumentRecord(record) {
|
||||
const documentStatus = ensureDocumentStatus(record)
|
||||
const imageAttachmentList = resolveAttachmentList(record.getString('document_image'))
|
||||
const videoAttachmentList = resolveAttachmentList(record.getString('document_video'))
|
||||
const firstImageAttachment = imageAttachmentList.attachments.length ? imageAttachmentList.attachments[0] : null
|
||||
@@ -180,7 +229,7 @@ function exportDocumentRecord(record) {
|
||||
document_share_count: record.get('document_share_count'),
|
||||
document_download_count: record.get('document_download_count'),
|
||||
document_favorite_count: record.get('document_favorite_count'),
|
||||
document_status: record.getString('document_status'),
|
||||
document_status: documentStatus,
|
||||
document_embedding_status: record.getString('document_embedding_status'),
|
||||
document_embedding_error: record.getString('document_embedding_error'),
|
||||
document_embedding_lasttime: String(record.get('document_embedding_lasttime') || ''),
|
||||
@@ -394,10 +443,13 @@ function createDocument(userOpenid, payload) {
|
||||
return $app.runInTransaction(function (txApp) {
|
||||
const collection = txApp.findCollectionByNameOrId('tbl_document')
|
||||
const record = new Record(collection)
|
||||
const effectDateValue = normalizeDateValue(payload.document_effect_date)
|
||||
const expiryDateValue = normalizeDateValue(payload.document_expiry_date)
|
||||
const documentStatus = calculateDocumentStatus(effectDateValue, expiryDateValue, payload.document_status)
|
||||
|
||||
record.set('document_id', targetDocumentId)
|
||||
record.set('document_effect_date', normalizeDateValue(payload.document_effect_date))
|
||||
record.set('document_expiry_date', normalizeDateValue(payload.document_expiry_date))
|
||||
record.set('document_effect_date', effectDateValue)
|
||||
record.set('document_expiry_date', expiryDateValue)
|
||||
record.set('document_type', payload.document_type || '')
|
||||
record.set('document_title', payload.document_title || '')
|
||||
record.set('document_subtitle', payload.document_subtitle || '')
|
||||
@@ -411,7 +463,7 @@ function createDocument(userOpenid, payload) {
|
||||
record.set('document_share_count', normalizeOptionalNumberValue(payload.document_share_count, 'document_share_count'))
|
||||
record.set('document_download_count', normalizeOptionalNumberValue(payload.document_download_count, 'document_download_count'))
|
||||
record.set('document_favorite_count', normalizeOptionalNumberValue(payload.document_favorite_count, 'document_favorite_count'))
|
||||
record.set('document_status', payload.document_status || '')
|
||||
record.set('document_status', documentStatus)
|
||||
record.set('document_embedding_status', payload.document_embedding_status || '')
|
||||
record.set('document_embedding_error', payload.document_embedding_error || '')
|
||||
record.set('document_embedding_lasttime', normalizeDateValue(payload.document_embedding_lasttime))
|
||||
@@ -446,9 +498,12 @@ function updateDocument(userOpenid, payload) {
|
||||
|
||||
return $app.runInTransaction(function (txApp) {
|
||||
const target = txApp.findRecordById('tbl_document', record.id)
|
||||
const effectDateValue = normalizeDateValue(payload.document_effect_date)
|
||||
const expiryDateValue = normalizeDateValue(payload.document_expiry_date)
|
||||
const documentStatus = calculateDocumentStatus(effectDateValue, expiryDateValue, payload.document_status)
|
||||
|
||||
target.set('document_effect_date', normalizeDateValue(payload.document_effect_date))
|
||||
target.set('document_expiry_date', normalizeDateValue(payload.document_expiry_date))
|
||||
target.set('document_effect_date', effectDateValue)
|
||||
target.set('document_expiry_date', expiryDateValue)
|
||||
target.set('document_type', payload.document_type || '')
|
||||
target.set('document_title', payload.document_title || '')
|
||||
target.set('document_subtitle', payload.document_subtitle || '')
|
||||
@@ -461,7 +516,7 @@ function updateDocument(userOpenid, payload) {
|
||||
target.set('document_share_count', normalizeOptionalNumberValue(payload.document_share_count, 'document_share_count'))
|
||||
target.set('document_download_count', normalizeOptionalNumberValue(payload.document_download_count, 'document_download_count'))
|
||||
target.set('document_favorite_count', normalizeOptionalNumberValue(payload.document_favorite_count, 'document_favorite_count'))
|
||||
target.set('document_status', payload.document_status || '')
|
||||
target.set('document_status', documentStatus)
|
||||
target.set('document_embedding_status', payload.document_embedding_status || '')
|
||||
target.set('document_embedding_error', payload.document_embedding_error || '')
|
||||
target.set('document_embedding_lasttime', normalizeDateValue(payload.document_embedding_lasttime))
|
||||
|
||||
@@ -19,6 +19,10 @@ function buildUserId() {
|
||||
return 'U' + date + suffix
|
||||
}
|
||||
|
||||
function buildUserConversId() {
|
||||
return 'UC-' + new Date().getTime() + '-' + $security.randomString(6)
|
||||
}
|
||||
|
||||
function buildGuid() {
|
||||
return $security.randomString(8) + '-' + $security.randomString(4) + '-' + $security.randomString(4) + '-' + $security.randomString(4) + '-' + $security.randomString(12)
|
||||
}
|
||||
@@ -186,7 +190,17 @@ function ensureUserId(record) {
|
||||
}
|
||||
}
|
||||
|
||||
function ensureUsersConversId(record) {
|
||||
if (!record.getString('users_convers_id')) {
|
||||
record.set('users_convers_id', buildUserConversId())
|
||||
}
|
||||
}
|
||||
|
||||
function saveAuthUserRecord(record) {
|
||||
ensureUserId(record)
|
||||
ensureUsersConversId(record)
|
||||
ensureAuthIdentity(record)
|
||||
|
||||
try {
|
||||
$app.save(record)
|
||||
} catch (err) {
|
||||
@@ -227,11 +241,9 @@ function authenticateWechatUser(payload) {
|
||||
const collection = $app.findCollectionByNameOrId('tbl_auth_users')
|
||||
const record = new Record(collection)
|
||||
record.set('openid', openid)
|
||||
ensureUserId(record)
|
||||
record.set('users_idtype', WECHAT_ID_TYPE)
|
||||
record.set('users_type', GUEST_USER_TYPE)
|
||||
record.set('users_auth_type', 0)
|
||||
ensureAuthIdentity(record)
|
||||
saveAuthUserRecord(record)
|
||||
|
||||
const user = enrichUser(record)
|
||||
@@ -278,7 +290,6 @@ function registerPlatformUser(payload) {
|
||||
const record = new Record(collection)
|
||||
|
||||
record.set('openid', platformOpenid)
|
||||
ensureUserId(record)
|
||||
record.set('users_idtype', MANAGE_PLATFORM_ID_TYPE)
|
||||
record.set('users_name', payload.users_name)
|
||||
record.set('users_phone', payload.users_phone)
|
||||
|
||||
@@ -17,18 +17,18 @@ routerAdd('GET', '/manage/dictionary-manage', function (e) {
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; background: linear-gradient(180deg, #f3f6fb 0%, #eef2ff 100%); color: #1f2937; }
|
||||
.container { max-width: 1240px; margin: 0 auto; padding: 32px 20px 60px; }
|
||||
.container { max-width: 1440px; margin: 0 auto; padding: 24px 14px 40px; }
|
||||
.topbar, .panel, .modal-card { background: rgba(255,255,255,0.96); border: 1px solid #e5e7eb; box-shadow: 0 18px 50px rgba(15, 23, 42, 0.08); }
|
||||
.topbar { border-radius: 24px; padding: 24px; margin-bottom: 24px; }
|
||||
.topbar { border-radius: 20px; padding: 18px; margin-bottom: 14px; }
|
||||
.topbar h1 { margin: 0 0 8px; font-size: 30px; }
|
||||
.topbar p { margin: 0; color: #4b5563; line-height: 1.7; }
|
||||
.actions { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 18px; }
|
||||
.actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 12px; }
|
||||
.btn { border: none; border-radius: 12px; padding: 10px 16px; font-weight: 600; cursor: pointer; }
|
||||
.btn-primary { background: #2563eb; color: #fff; }
|
||||
.btn-secondary { background: #e0e7ff; color: #1e3a8a; }
|
||||
.btn-danger { background: #dc2626; color: #fff; }
|
||||
.btn-light { background: #f8fafc; color: #334155; border: 1px solid #dbe3f0; }
|
||||
.panel { border-radius: 24px; padding: 24px; }
|
||||
.panel { border-radius: 20px; padding: 18px; }
|
||||
.toolbar { display: grid; grid-template-columns: 1.3fr 1fr 1fr auto auto; gap: 12px; margin-bottom: 18px; }
|
||||
input, textarea, select { width: 100%; border: 1px solid #cbd5e1; border-radius: 12px; padding: 10px 12px; font-size: 14px; background: #fff; }
|
||||
textarea { min-height: 88px; resize: vertical; }
|
||||
@@ -47,9 +47,9 @@ routerAdd('GET', '/manage/dictionary-manage', function (e) {
|
||||
.status.success { color: #15803d; }
|
||||
.status.error { color: #b91c1c; }
|
||||
.auth-box { display: grid; grid-template-columns: 1fr auto; gap: 12px; margin-top: 18px; }
|
||||
.modal { position: fixed; inset: 0; display: none; align-items: center; justify-content: center; background: rgba(15, 23, 42, 0.4); padding: 20px; }
|
||||
.modal { position: fixed; inset: 0; display: none; align-items: center; justify-content: center; background: rgba(15, 23, 42, 0.4); padding: 14px; }
|
||||
.modal.show { display: flex; }
|
||||
.modal-card { width: min(920px, 100%); border-radius: 24px; padding: 24px; max-height: 90vh; overflow: auto; }
|
||||
.modal-card { width: min(1104px, 100%); border-radius: 20px; padding: 18px; max-height: 90vh; overflow: auto; }
|
||||
.modal-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 16px; }
|
||||
.modal-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
|
||||
.full { grid-column: 1 / -1; }
|
||||
@@ -57,6 +57,16 @@ routerAdd('GET', '/manage/dictionary-manage', function (e) {
|
||||
.item-table table { border-radius: 0; }
|
||||
.modal-actions { display: flex; justify-content: space-between; flex-wrap: wrap; gap: 12px; margin-top: 16px; }
|
||||
.empty { text-align: center; padding: 32px 16px; color: #64748b; }
|
||||
.thumb { width: 48px; height: 48px; border-radius: 10px; object-fit: cover; border: 1px solid #dbe3f0; background: #fff; }
|
||||
.thumb-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
||||
.thumb-meta { font-size: 12px; color: #64748b; word-break: break-all; }
|
||||
.enum-preview-item { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
|
||||
.enum-preview-more { margin-top: 8px; display: none; }
|
||||
.enum-preview-toggle { margin-top: 6px; }
|
||||
.image-upload-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 8px; margin-top: 10px; }
|
||||
.image-upload-row input[type="file"] { width: 100%; min-width: 0; }
|
||||
.icon-btn { min-width: 36px; padding: 6px 10px; line-height: 1; font-size: 18px; }
|
||||
.drop-tip { color: #64748b; font-size: 12px; margin-top: 6px; }
|
||||
@media (max-width: 960px) {
|
||||
.toolbar, .auth-box, .modal-grid { grid-template-columns: 1fr; }
|
||||
table, thead, tbody, th, td, tr { display: block; }
|
||||
@@ -71,10 +81,8 @@ routerAdd('GET', '/manage/dictionary-manage', function (e) {
|
||||
<div class="container">
|
||||
<section class="topbar">
|
||||
<h1>字典管理</h1>
|
||||
<p>页面仅允许 ManagePlatform 用户操作。当前请求会自动使用登录页保存到 localStorage 的 token。</p>
|
||||
<div class="actions">
|
||||
<a class="btn btn-light" href="/pb/manage">返回主页</a>
|
||||
<a class="btn btn-light" href="/pb/manage/login">登录页</a>
|
||||
<button class="btn btn-primary" id="createBtn" type="button">新增字典</button>
|
||||
<button class="btn btn-danger" id="logoutBtn" type="button">退出登录</button>
|
||||
</div>
|
||||
@@ -114,7 +122,6 @@ routerAdd('GET', '/manage/dictionary-manage', function (e) {
|
||||
<div class="modal-header">
|
||||
<div>
|
||||
<h2 id="modalTitle" style="margin:0;">新增字典</h2>
|
||||
<div class="muted">三个聚合字段将以 JSON 字符串形式保存,并在读取时自动还原为 items。</div>
|
||||
</div>
|
||||
<button class="btn btn-light" id="closeModalBtn" type="button">关闭</button>
|
||||
</div>
|
||||
@@ -140,8 +147,8 @@ routerAdd('GET', '/manage/dictionary-manage', function (e) {
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>枚举值</th>
|
||||
<th>描述</th>
|
||||
<th>图片</th>
|
||||
<th>排序</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
@@ -165,11 +172,15 @@ routerAdd('GET', '/manage/dictionary-manage', function (e) {
|
||||
<script>
|
||||
const API_BASE = '/pb/api'
|
||||
const tokenKey = 'pb_manage_token'
|
||||
const enumChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
const state = {
|
||||
list: [],
|
||||
mode: 'create',
|
||||
editingOriginalName: '',
|
||||
items: [],
|
||||
enumSeed: '',
|
||||
enumCounter: 1,
|
||||
expandedPreviewKey: '',
|
||||
}
|
||||
|
||||
const statusEl = document.getElementById('status')
|
||||
@@ -224,6 +235,38 @@ routerAdd('GET', '/manage/dictionary-manage', function (e) {
|
||||
return data.data
|
||||
}
|
||||
|
||||
async function uploadAttachment(file, label) {
|
||||
const token = getToken()
|
||||
if (!token) {
|
||||
localStorage.removeItem('pb_manage_logged_in')
|
||||
window.location.replace('/pb/manage/login')
|
||||
throw new Error('登录状态已失效,请重新登录')
|
||||
}
|
||||
|
||||
const form = new FormData()
|
||||
form.append('attachments_link', file)
|
||||
form.append('attachments_filename', file.name || '')
|
||||
form.append('attachments_filetype', file.type || '')
|
||||
form.append('attachments_size', String(file.size || 0))
|
||||
form.append('attachments_status', 'active')
|
||||
form.append('attachments_remark', 'dictionary-manage:' + label)
|
||||
|
||||
const res = await fetch(API_BASE + '/attachment/upload', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + token,
|
||||
},
|
||||
body: form,
|
||||
})
|
||||
|
||||
const data = await res.json()
|
||||
if (!res.ok || !data || data.code >= 400) {
|
||||
throw new Error((data && data.msg) || '上传图片失败')
|
||||
}
|
||||
|
||||
return data.data
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value || '')
|
||||
.replace(/&/g, '&')
|
||||
@@ -233,11 +276,86 @@ routerAdd('GET', '/manage/dictionary-manage', function (e) {
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
function renderItemsPreview(items) {
|
||||
function randomEnumSeed() {
|
||||
const first = enumChars[Math.floor(Math.random() * enumChars.length)]
|
||||
const second = enumChars[Math.floor(Math.random() * enumChars.length)]
|
||||
return first + second
|
||||
}
|
||||
|
||||
function ensureEnumSeed() {
|
||||
if (!state.enumSeed) {
|
||||
state.enumSeed = randomEnumSeed()
|
||||
}
|
||||
}
|
||||
|
||||
function nextAutoEnum(usedSet) {
|
||||
ensureEnumSeed()
|
||||
let guard = 0
|
||||
while (guard < 10000) {
|
||||
const candidate = state.enumSeed + String(state.enumCounter)
|
||||
state.enumCounter += 1
|
||||
if (!usedSet.has(candidate)) {
|
||||
return candidate
|
||||
}
|
||||
guard += 1
|
||||
}
|
||||
|
||||
state.enumSeed = randomEnumSeed()
|
||||
state.enumCounter = 1
|
||||
return nextAutoEnum(usedSet)
|
||||
}
|
||||
|
||||
function normalizeItemEnums() {
|
||||
const used = new Set()
|
||||
state.items = state.items.map(function (item) {
|
||||
const current = {
|
||||
enum: String((item && item.enum) || '').trim(),
|
||||
description: String((item && item.description) || ''),
|
||||
image: (item && item.image) || '',
|
||||
imageUrl: (item && item.imageUrl) || '',
|
||||
imageAttachment: (item && item.imageAttachment) || null,
|
||||
sortOrder: Number((item && item.sortOrder) || 0),
|
||||
}
|
||||
|
||||
if (!current.enum || used.has(current.enum)) {
|
||||
current.enum = nextAutoEnum(used)
|
||||
}
|
||||
|
||||
used.add(current.enum)
|
||||
if (!Number.isFinite(current.sortOrder) || current.sortOrder <= 0) {
|
||||
current.sortOrder = used.size
|
||||
}
|
||||
|
||||
return current
|
||||
})
|
||||
}
|
||||
|
||||
function renderEnumPreviewItem(item) {
|
||||
const desc = escapeHtml(item && item.description ? item.description : '(无描述)')
|
||||
const imageHtml = item && item.imageUrl
|
||||
? '<img class="thumb" src="' + escapeHtml(item.imageUrl) + '" alt="" />'
|
||||
: '<span class="muted">无图</span>'
|
||||
return '<div class="enum-preview-item"><span>' + desc + '</span>' + imageHtml + '</div>'
|
||||
}
|
||||
|
||||
function renderItemsPreview(items, previewKey, isExpanded) {
|
||||
if (!items || !items.length) return '<span class="muted">无</span>'
|
||||
return items.map(function (item) {
|
||||
return '<div><strong>' + escapeHtml(item.enum) + '</strong> → ' + escapeHtml(item.description) + '(排序 ' + escapeHtml(item.sortOrder) + ')</div>'
|
||||
|
||||
const first = renderEnumPreviewItem(items[0])
|
||||
if (items.length === 1) {
|
||||
return first
|
||||
}
|
||||
|
||||
const rest = items.slice(1).map(function (item) {
|
||||
return renderEnumPreviewItem(item)
|
||||
}).join('')
|
||||
const hiddenCount = items.length - 1
|
||||
const moreStyle = isExpanded ? 'display:block;' : 'display:none;'
|
||||
const toggleText = isExpanded ? '收起' : ('展开其余 ' + hiddenCount + ' 项')
|
||||
|
||||
return first
|
||||
+ '<div class="enum-preview-more" style="' + moreStyle + '">' + rest + '</div>'
|
||||
+ '<button class="btn btn-light enum-preview-toggle" type="button" onclick="window.__toggleEnumPreview(\\'' + previewKey + '\\')">' + toggleText + '</button>'
|
||||
}
|
||||
|
||||
function renderTable(list) {
|
||||
@@ -246,14 +364,15 @@ routerAdd('GET', '/manage/dictionary-manage', function (e) {
|
||||
return
|
||||
}
|
||||
|
||||
tableBody.innerHTML = list.map(function (item) {
|
||||
tableBody.innerHTML = list.map(function (item, index) {
|
||||
const enabledClass = item.dict_word_is_enabled ? 'badge badge-on' : 'badge badge-off'
|
||||
const enabledText = item.dict_word_is_enabled ? '启用' : '禁用'
|
||||
const previewKey = String(index)
|
||||
return '<tr>'
|
||||
+ '<td data-label="字典名称"><input class="inline-input" data-field="dict_name" data-name="' + escapeHtml(item.dict_name) + '" value="' + escapeHtml(item.dict_name) + '" /></td>'
|
||||
+ '<td data-label="启用"><select class="inline-input" data-field="dict_word_is_enabled" data-name="' + escapeHtml(item.dict_name) + '"><option value="true"' + (item.dict_word_is_enabled ? ' selected' : '') + '>启用</option><option value="false"' + (!item.dict_word_is_enabled ? ' selected' : '') + '>禁用</option></select><div class="' + enabledClass + '" style="margin-top:8px;">' + enabledText + '</div></td>'
|
||||
+ '<td data-label="备注"><textarea class="inline-input" data-field="dict_word_remark" data-name="' + escapeHtml(item.dict_name) + '">' + escapeHtml(item.dict_word_remark) + '</textarea></td>'
|
||||
+ '<td data-label="枚举项">' + renderItemsPreview(item.items) + '</td>'
|
||||
+ '<td data-label="枚举项">' + renderItemsPreview(item.items, previewKey, state.expandedPreviewKey === previewKey) + '</td>'
|
||||
+ '<td data-label="创建时间"><span class="muted">' + escapeHtml(item.created) + '</span></td>'
|
||||
+ '<td data-label="操作">'
|
||||
+ '<div style="display:flex;flex-wrap:wrap;gap:8px;">'
|
||||
@@ -269,13 +388,25 @@ routerAdd('GET', '/manage/dictionary-manage', function (e) {
|
||||
function openModal(mode, record) {
|
||||
state.mode = mode
|
||||
state.editingOriginalName = record ? record.dict_name : ''
|
||||
state.enumSeed = randomEnumSeed()
|
||||
state.enumCounter = 1
|
||||
modalTitle.textContent = mode === 'create' ? '新增字典' : '编辑枚举项'
|
||||
dictNameInput.value = record ? record.dict_name : ''
|
||||
enabledInput.value = record && !record.dict_word_is_enabled ? 'false' : 'true'
|
||||
remarkInput.value = record ? (record.dict_word_remark || '') : ''
|
||||
state.items = record && Array.isArray(record.items) && record.items.length
|
||||
? record.items.map(function (item) { return { enum: item.enum, description: item.description, sortOrder: item.sortOrder } })
|
||||
: [{ enum: '', description: '', sortOrder: 1 }]
|
||||
? record.items.map(function (item) {
|
||||
return {
|
||||
enum: item.enum,
|
||||
description: item.description,
|
||||
image: item.image || '',
|
||||
imageUrl: item.imageUrl || '',
|
||||
imageAttachment: item.imageAttachment || null,
|
||||
sortOrder: item.sortOrder,
|
||||
}
|
||||
})
|
||||
: [{ enum: '', description: '', image: '', imageUrl: '', imageAttachment: null, sortOrder: 1 }]
|
||||
normalizeItemEnums()
|
||||
renderItemsEditor()
|
||||
editorModal.classList.add('show')
|
||||
}
|
||||
@@ -284,36 +415,110 @@ routerAdd('GET', '/manage/dictionary-manage', function (e) {
|
||||
editorModal.classList.remove('show')
|
||||
}
|
||||
|
||||
function scrollEditorModalToBottom() {
|
||||
const modalCard = editorModal.querySelector('.modal-card')
|
||||
if (!modalCard) {
|
||||
return
|
||||
}
|
||||
|
||||
const doScroll = function () {
|
||||
modalCard.scrollTo({
|
||||
top: modalCard.scrollHeight,
|
||||
behavior: 'smooth',
|
||||
})
|
||||
}
|
||||
|
||||
if (typeof requestAnimationFrame === 'function') {
|
||||
requestAnimationFrame(function () {
|
||||
requestAnimationFrame(doScroll)
|
||||
})
|
||||
} else {
|
||||
setTimeout(doScroll, 40)
|
||||
}
|
||||
|
||||
// Fallback: handle heavy DOM updates that render slightly later.
|
||||
setTimeout(doScroll, 120)
|
||||
}
|
||||
|
||||
function renderItemsEditor() {
|
||||
itemsBody.innerHTML = state.items.map(function (item, index) {
|
||||
const imageCell = item.imageUrl
|
||||
? '<div class="thumb-row"><img class="thumb" src="' + escapeHtml(item.imageUrl) + '" alt="" /><div class="thumb-meta">' + escapeHtml(item.image || '') + '</div></div>'
|
||||
: '<div class="muted">未上传图片</div>'
|
||||
return '<tr>'
|
||||
+ '<td><input data-item-field="enum" data-index="' + index + '" value="' + escapeHtml(item.enum) + '" /></td>'
|
||||
+ '<td><input data-item-field="description" data-index="' + index + '" value="' + escapeHtml(item.description) + '" /></td>'
|
||||
+ '<td ondragover="window.__allowItemDrop(event)" ondrop="window.__dropItemImage(' + index + ', event)">'
|
||||
+ imageCell
|
||||
+ '<div class="image-upload-row">'
|
||||
+ '<input type="file" accept="image/*" onchange="window.__uploadItemImage(' + index + ', this)" />'
|
||||
+ '<button class="btn btn-light icon-btn" type="button" title="删除图片" onclick="window.__clearItemImage(' + index + ')">🗑️</button>'
|
||||
+ '</div>'
|
||||
+ '<div class="drop-tip">支持拖拽图片到本区域上传</div>'
|
||||
+ '</td>'
|
||||
+ '<td><input type="number" data-item-field="sortOrder" data-index="' + index + '" value="' + escapeHtml(item.sortOrder) + '" /></td>'
|
||||
+ '<td><button class="btn btn-danger" type="button" onclick="window.__removeItem(' + index + ')">删除</button></td>'
|
||||
+ '</tr>'
|
||||
}).join('')
|
||||
}
|
||||
|
||||
async function setItemImageFromFile(index, file) {
|
||||
if (!file) {
|
||||
return
|
||||
}
|
||||
|
||||
syncItemsStateFromEditor()
|
||||
if (!state.items[index]) {
|
||||
return
|
||||
}
|
||||
|
||||
setStatus('正在上传字典项图片...', '')
|
||||
try {
|
||||
const attachment = await uploadAttachment(file, 'dict-item-' + (index + 1))
|
||||
state.items[index].image = attachment.attachments_id
|
||||
state.items[index].imageUrl = attachment.attachments_url || ''
|
||||
state.items[index].imageAttachment = attachment
|
||||
renderItemsEditor()
|
||||
setStatus('字典项图片上传成功。', 'success')
|
||||
} catch (err) {
|
||||
setStatus(err.message || '字典项图片上传失败', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
function collectItemsFromEditor() {
|
||||
const rows = Array.from(itemsBody.querySelectorAll('tr'))
|
||||
return rows.map(function (row, index) {
|
||||
const enumInput = row.querySelector('[data-item-field="enum"]')
|
||||
const descriptionInput = row.querySelector('[data-item-field="description"]')
|
||||
const sortOrderInput = row.querySelector('[data-item-field="sortOrder"]')
|
||||
return {
|
||||
enum: enumInput.value.trim(),
|
||||
enum: state.items[index] ? String(state.items[index].enum || '') : '',
|
||||
description: descriptionInput.value.trim(),
|
||||
image: state.items[index] ? (state.items[index].image || '') : '',
|
||||
imageUrl: state.items[index] ? (state.items[index].imageUrl || '') : '',
|
||||
imageAttachment: state.items[index] ? (state.items[index].imageAttachment || null) : null,
|
||||
sortOrder: Number(sortOrderInput.value || index + 1),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function syncItemsStateFromEditor() {
|
||||
const rows = itemsBody.querySelectorAll('tr')
|
||||
if (!rows.length) {
|
||||
return
|
||||
}
|
||||
state.items = collectItemsFromEditor()
|
||||
}
|
||||
|
||||
async function uploadItemImage(index, inputEl) {
|
||||
const file = inputEl && inputEl.files && inputEl.files[0]
|
||||
await setItemImageFromFile(index, file)
|
||||
}
|
||||
|
||||
async function loadList() {
|
||||
setStatus('正在查询字典列表...', '')
|
||||
try {
|
||||
const data = await request(API_BASE + '/dictionary/list', { keyword: keywordInput.value.trim() })
|
||||
state.list = data.items || []
|
||||
state.expandedPreviewKey = ''
|
||||
renderTable(state.list)
|
||||
setStatus('查询成功,共 ' + state.list.length + ' 条。', 'success')
|
||||
} catch (err) {
|
||||
@@ -332,6 +537,7 @@ routerAdd('GET', '/manage/dictionary-manage', function (e) {
|
||||
try {
|
||||
const data = await request(API_BASE + '/dictionary/detail', { dict_name: dictName })
|
||||
state.list = [data]
|
||||
state.expandedPreviewKey = ''
|
||||
renderTable(state.list)
|
||||
setStatus('查询详情成功。', 'success')
|
||||
} catch (err) {
|
||||
@@ -340,7 +546,9 @@ routerAdd('GET', '/manage/dictionary-manage', function (e) {
|
||||
}
|
||||
|
||||
async function saveModalRecord() {
|
||||
const items = collectItemsFromEditor()
|
||||
syncItemsStateFromEditor()
|
||||
normalizeItemEnums()
|
||||
const items = state.items
|
||||
const payload = {
|
||||
dict_name: dictNameInput.value.trim(),
|
||||
original_dict_name: state.editingOriginalName,
|
||||
@@ -420,11 +628,38 @@ routerAdd('GET', '/manage/dictionary-manage', function (e) {
|
||||
|
||||
window.__saveInline = saveInline
|
||||
window.__deleteRow = deleteRow
|
||||
window.__toggleEnumPreview = function (previewKey) {
|
||||
state.expandedPreviewKey = state.expandedPreviewKey === previewKey ? '' : previewKey
|
||||
renderTable(state.list)
|
||||
}
|
||||
window.__uploadItemImage = uploadItemImage
|
||||
window.__allowItemDrop = function (event) {
|
||||
event.preventDefault()
|
||||
}
|
||||
window.__dropItemImage = function (index, event) {
|
||||
event.preventDefault()
|
||||
const files = event.dataTransfer && event.dataTransfer.files
|
||||
const file = files && files[0]
|
||||
setItemImageFromFile(index, file)
|
||||
}
|
||||
window.__clearItemImage = function (index) {
|
||||
syncItemsStateFromEditor()
|
||||
if (!state.items[index]) {
|
||||
return
|
||||
}
|
||||
state.items[index].image = ''
|
||||
state.items[index].imageUrl = ''
|
||||
state.items[index].imageAttachment = null
|
||||
renderItemsEditor()
|
||||
setStatus('已清空该枚举项图片。', 'success')
|
||||
}
|
||||
window.__removeItem = function (index) {
|
||||
syncItemsStateFromEditor()
|
||||
state.items.splice(index, 1)
|
||||
if (!state.items.length) {
|
||||
state.items.push({ enum: '', description: '', sortOrder: 1 })
|
||||
state.items.push({ enum: '', description: '', image: '', imageUrl: '', imageAttachment: null, sortOrder: 1 })
|
||||
}
|
||||
normalizeItemEnums()
|
||||
renderItemsEditor()
|
||||
}
|
||||
|
||||
@@ -441,8 +676,11 @@ routerAdd('GET', '/manage/dictionary-manage', function (e) {
|
||||
document.getElementById('closeModalBtn').addEventListener('click', closeModal)
|
||||
document.getElementById('cancelBtn').addEventListener('click', closeModal)
|
||||
document.getElementById('addItemBtn').addEventListener('click', function () {
|
||||
state.items.push({ enum: '', description: '', sortOrder: state.items.length + 1 })
|
||||
syncItemsStateFromEditor()
|
||||
const used = new Set(state.items.map(function (item) { return String(item.enum || '') }))
|
||||
state.items.push({ enum: nextAutoEnum(used), description: '', image: '', imageUrl: '', imageAttachment: null, sortOrder: state.items.length + 1 })
|
||||
renderItemsEditor()
|
||||
scrollEditorModalToBottom()
|
||||
})
|
||||
document.getElementById('saveBtn').addEventListener('click', saveModalRecord)
|
||||
document.getElementById('logoutBtn').addEventListener('click', function () {
|
||||
@@ -452,9 +690,11 @@ routerAdd('GET', '/manage/dictionary-manage', function (e) {
|
||||
localStorage.removeItem('pb_manage_login_time')
|
||||
window.location.replace('/pb/manage/login')
|
||||
})
|
||||
|
||||
loadList()
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
return e.html(200, html)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -17,9 +17,9 @@ routerAdd('GET', '/manage/document-manage', function (e) {
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; background: linear-gradient(180deg, #eef6ff 0%, #f8fafc 100%); color: #1f2937; }
|
||||
.container { max-width: 1280px; margin: 0 auto; padding: 32px 20px 64px; }
|
||||
.panel { background: rgba(255,255,255,0.96); border: 1px solid #e5e7eb; box-shadow: 0 18px 50px rgba(15, 23, 42, 0.08); border-radius: 24px; padding: 24px; }
|
||||
.panel + .panel { margin-top: 24px; }
|
||||
.container { max-width: 1440px; margin: 0 auto; padding: 24px 14px 40px; }
|
||||
.panel { background: rgba(255,255,255,0.96); border: 1px solid #e5e7eb; box-shadow: 0 18px 50px rgba(15, 23, 42, 0.08); border-radius: 20px; padding: 18px; }
|
||||
.panel + .panel { margin-top: 14px; }
|
||||
h1, h2 { margin-top: 0; }
|
||||
p { color: #4b5563; line-height: 1.7; }
|
||||
.actions, .form-actions, .toolbar, .table-actions, .file-actions { display: flex; flex-wrap: wrap; gap: 12px; }
|
||||
@@ -48,6 +48,16 @@ routerAdd('GET', '/manage/document-manage', function (e) {
|
||||
.editor-banner { display: inline-flex; align-items: center; gap: 10px; padding: 8px 12px; border-radius: 999px; background: #eff6ff; color: #1d4ed8; font-size: 13px; font-weight: 700; }
|
||||
.file-group { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; margin-top: 16px; }
|
||||
.file-box { border: 1px solid #dbe3f0; border-radius: 18px; padding: 16px; background: #f8fbff; }
|
||||
.option-box { border: 1px solid #dbe3f0; border-radius: 16px; background: #f8fbff; padding: 14px; }
|
||||
.option-list { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; margin-top: 12px; }
|
||||
.option-item { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 12px; background: #fff; border: 1px solid #e5e7eb; min-height: 48px; }
|
||||
.option-item input[type="checkbox"] { width: auto; margin: 0; }
|
||||
.option-item span { display: block; word-break: break-word; }
|
||||
.selection-tags { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
|
||||
.selection-tag { display: inline-flex; align-items: center; padding: 4px 10px; border-radius: 999px; background: #eff6ff; color: #1d4ed8; font-size: 12px; font-weight: 700; }
|
||||
.choice-switch { display: inline-flex; gap: 8px; padding: 6px; border-radius: 14px; border: 1px solid #dbe3f0; background: #f8fbff; }
|
||||
.choice-switch button { border: 1px solid transparent; background: transparent; color: #475569; padding: 9px 18px; border-radius: 10px; font-weight: 700; cursor: pointer; }
|
||||
.choice-switch button.active { background: #2563eb; color: #fff; border-color: #2563eb; }
|
||||
.dropzone { margin-top: 8px; border: 2px dashed #bfdbfe; border-radius: 16px; background: #ffffff; padding: 16px; transition: border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease; cursor: pointer; }
|
||||
.dropzone.dragover { border-color: #2563eb; background: #eff6ff; box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.08); }
|
||||
.dropzone-title { font-weight: 700; color: #1e3a8a; }
|
||||
@@ -60,6 +70,7 @@ routerAdd('GET', '/manage/document-manage', function (e) {
|
||||
.file-meta { color: #64748b; font-size: 12px; margin-top: 6px; word-break: break-all; }
|
||||
@media (max-width: 960px) {
|
||||
.grid, .file-group { grid-template-columns: 1fr; }
|
||||
.option-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
table, thead, tbody, th, td, tr { display: block; }
|
||||
thead { display: none; }
|
||||
tr { margin-bottom: 14px; border: 1px solid #e5e7eb; border-radius: 16px; overflow: hidden; }
|
||||
@@ -72,10 +83,8 @@ routerAdd('GET', '/manage/document-manage', function (e) {
|
||||
<div class="container">
|
||||
<section class="panel">
|
||||
<h1>文档管理</h1>
|
||||
<p>页面会通过 <code>/pb/api/*</code> 调用 PocketBase hooks。图片和视频都支持多选上传;编辑已有文档时,会先回显当前已绑定的多图片、多视频,并支持从文档中移除或继续追加附件。</p>
|
||||
<div class="actions">
|
||||
<a class="btn btn-light" href="/pb/manage">返回主页</a>
|
||||
<a class="btn btn-light" href="/pb/manage/login">登录页</a>
|
||||
<button class="btn btn-light" id="reloadBtn" type="button">刷新列表</button>
|
||||
<button class="btn btn-light" id="createModeBtn" type="button">新建模式</button>
|
||||
<button class="btn btn-danger" id="logoutBtn" type="button">退出登录</button>
|
||||
@@ -87,7 +96,6 @@ routerAdd('GET', '/manage/document-manage', function (e) {
|
||||
<div class="toolbar" style="justify-content:space-between;align-items:center;">
|
||||
<div>
|
||||
<h2 id="formTitle">新增文档</h2>
|
||||
<div class="muted">只有文档标题和文档类型为必填,其余字段允许为空。</div>
|
||||
</div>
|
||||
<div class="editor-banner" id="editorMode">当前模式:新建</div>
|
||||
</div>
|
||||
@@ -97,16 +105,31 @@ routerAdd('GET', '/manage/document-manage', function (e) {
|
||||
<input id="documentTitle" placeholder="请输入文档标题" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="documentType">文档类型</label>
|
||||
<input id="documentType" placeholder="例如:说明书、新闻、常见故障" />
|
||||
<label for="embeddingStatus">嵌入状态</label>
|
||||
<input id="embeddingStatus" placeholder="可为空" />
|
||||
</div>
|
||||
<div class="full">
|
||||
<label for="documentSubtitle">副标题</label>
|
||||
<input id="documentSubtitle" placeholder="可选" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="documentTypeSource">文档类型</label>
|
||||
<div class="option-box">
|
||||
<select id="documentTypeSource">
|
||||
<option value="">请选择数据来源字典</option>
|
||||
</select>
|
||||
<div class="option-list" id="documentTypeOptions"></div>
|
||||
<div class="selection-tags" id="documentTypeTags"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="documentStatus">文档状态</label>
|
||||
<input id="documentStatus" placeholder="可为空" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="embeddingStatus">嵌入状态</label>
|
||||
<input id="embeddingStatus" placeholder="可为空" />
|
||||
<input id="documentStatus" type="hidden" value="有效" />
|
||||
<div class="choice-switch" id="documentStatusSwitch">
|
||||
<button type="button" data-status-value="有效" class="active">有效</button>
|
||||
<button type="button" data-status-value="过期">过期</button>
|
||||
</div>
|
||||
<div class="hint">系统会根据生效日期和到期日期自动切换状态;都不填写时默认有效。</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="effectDate">生效日期</label>
|
||||
@@ -116,10 +139,6 @@ routerAdd('GET', '/manage/document-manage', function (e) {
|
||||
<label for="expiryDate">到期日期</label>
|
||||
<input id="expiryDate" type="date" />
|
||||
</div>
|
||||
<div class="full">
|
||||
<label for="documentSubtitle">副标题</label>
|
||||
<input id="documentSubtitle" placeholder="可选" />
|
||||
</div>
|
||||
<div class="full">
|
||||
<label for="documentSummary">摘要</label>
|
||||
<textarea id="documentSummary" placeholder="请输入文档摘要"></textarea>
|
||||
@@ -137,21 +156,36 @@ routerAdd('GET', '/manage/document-manage', function (e) {
|
||||
<input id="vectorVersion" placeholder="可选" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="documentKeywords">关键词</label>
|
||||
<input id="documentKeywords" placeholder="多个值用 | 分隔" />
|
||||
<div class="hint">例如:安装|配置|排障</div>
|
||||
<label>关键词</label>
|
||||
<div class="option-box">
|
||||
<div class="hint">可不选,也可多选。</div>
|
||||
<div class="option-list" id="documentKeywordsOptions"></div>
|
||||
<div class="selection-tags" id="documentKeywordsTags"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="productCategories">适用产品类别</label>
|
||||
<input id="productCategories" placeholder="多个值用 | 分隔" />
|
||||
<label>产品关联文档</label>
|
||||
<div class="option-box">
|
||||
<div class="hint">可不选,也可多选。</div>
|
||||
<div class="option-list" id="productCategoriesOptions"></div>
|
||||
<div class="selection-tags" id="productCategoriesTags"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="applicationScenarios">适用场景</label>
|
||||
<input id="applicationScenarios" placeholder="多个值用 | 分隔" />
|
||||
<label>筛选依据</label>
|
||||
<div class="option-box">
|
||||
<div class="hint">可不选,也可多选。</div>
|
||||
<div class="option-list" id="applicationScenariosOptions"></div>
|
||||
<div class="selection-tags" id="applicationScenariosTags"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="hotelType">适用酒店类型</label>
|
||||
<input id="hotelType" placeholder="多个值用 | 分隔" />
|
||||
<label>适用场景</label>
|
||||
<div class="option-box">
|
||||
<div class="hint">可不选,也可多选。</div>
|
||||
<div class="option-list" id="hotelTypeOptions"></div>
|
||||
<div class="selection-tags" id="hotelTypeTags"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="full">
|
||||
<label for="documentRemark">备注</label>
|
||||
@@ -164,7 +198,6 @@ routerAdd('GET', '/manage/document-manage', function (e) {
|
||||
<label for="imageFile">新增图片</label>
|
||||
<div class="dropzone" id="imageDropzone">
|
||||
<div class="dropzone-title">拖拽图片到这里,或点击选择文件</div>
|
||||
<div class="dropzone-text">支持从 Windows 文件夹直接拖入;选择后先进入待上传区,保存文档时才会真正上传。</div>
|
||||
<input id="imageFile" type="file" multiple />
|
||||
</div>
|
||||
<div class="file-list" id="imageCurrentList"></div>
|
||||
@@ -175,7 +208,6 @@ routerAdd('GET', '/manage/document-manage', function (e) {
|
||||
<label for="videoFile">新增视频</label>
|
||||
<div class="dropzone" id="videoDropzone">
|
||||
<div class="dropzone-title">拖拽视频到这里,或点击选择文件</div>
|
||||
<div class="dropzone-text">支持从 Windows 文件夹直接拖入;选择后先进入待上传区,保存文档时才会真正上传。</div>
|
||||
<input id="videoFile" type="file" multiple />
|
||||
</div>
|
||||
<div class="file-list" id="videoCurrentList"></div>
|
||||
@@ -195,7 +227,6 @@ routerAdd('GET', '/manage/document-manage', function (e) {
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>document_id</th>
|
||||
<th>标题</th>
|
||||
<th>类型/状态</th>
|
||||
<th>附件链接</th>
|
||||
@@ -224,9 +255,20 @@ routerAdd('GET', '/manage/document-manage', function (e) {
|
||||
const videoPendingListEl = document.getElementById('videoPendingList')
|
||||
const imageDropzoneEl = document.getElementById('imageDropzone')
|
||||
const videoDropzoneEl = document.getElementById('videoDropzone')
|
||||
const documentTypeSourceEl = document.getElementById('documentTypeSource')
|
||||
const documentTypeOptionsEl = document.getElementById('documentTypeOptions')
|
||||
const documentTypeTagsEl = document.getElementById('documentTypeTags')
|
||||
const documentStatusSwitchEl = document.getElementById('documentStatusSwitch')
|
||||
const documentKeywordsOptionsEl = document.getElementById('documentKeywordsOptions')
|
||||
const documentKeywordsTagsEl = document.getElementById('documentKeywordsTags')
|
||||
const productCategoriesOptionsEl = document.getElementById('productCategoriesOptions')
|
||||
const productCategoriesTagsEl = document.getElementById('productCategoriesTags')
|
||||
const applicationScenariosOptionsEl = document.getElementById('applicationScenariosOptions')
|
||||
const applicationScenariosTagsEl = document.getElementById('applicationScenariosTags')
|
||||
const hotelTypeOptionsEl = document.getElementById('hotelTypeOptions')
|
||||
const hotelTypeTagsEl = document.getElementById('hotelTypeTags')
|
||||
const fields = {
|
||||
documentTitle: document.getElementById('documentTitle'),
|
||||
documentType: document.getElementById('documentType'),
|
||||
documentStatus: document.getElementById('documentStatus'),
|
||||
embeddingStatus: document.getElementById('embeddingStatus'),
|
||||
effectDate: document.getElementById('effectDate'),
|
||||
@@ -236,14 +278,32 @@ routerAdd('GET', '/manage/document-manage', function (e) {
|
||||
documentContent: document.getElementById('documentContent'),
|
||||
relationModel: document.getElementById('relationModel'),
|
||||
vectorVersion: document.getElementById('vectorVersion'),
|
||||
documentKeywords: document.getElementById('documentKeywords'),
|
||||
productCategories: document.getElementById('productCategories'),
|
||||
applicationScenarios: document.getElementById('applicationScenarios'),
|
||||
hotelType: document.getElementById('hotelType'),
|
||||
documentRemark: document.getElementById('documentRemark'),
|
||||
imageFile: document.getElementById('imageFile'),
|
||||
videoFile: document.getElementById('videoFile'),
|
||||
}
|
||||
const dictionaryFieldConfig = {
|
||||
documentKeywords: {
|
||||
dictName: '文档-关键词',
|
||||
optionsEl: documentKeywordsOptionsEl,
|
||||
tagsEl: documentKeywordsTagsEl,
|
||||
},
|
||||
productCategories: {
|
||||
dictName: '文档-产品关联文档',
|
||||
optionsEl: productCategoriesOptionsEl,
|
||||
tagsEl: productCategoriesTagsEl,
|
||||
},
|
||||
applicationScenarios: {
|
||||
dictName: '文档-筛选依据',
|
||||
optionsEl: applicationScenariosOptionsEl,
|
||||
tagsEl: applicationScenariosTagsEl,
|
||||
},
|
||||
hotelType: {
|
||||
dictName: '文档-适用场景',
|
||||
optionsEl: hotelTypeOptionsEl,
|
||||
tagsEl: hotelTypeTagsEl,
|
||||
},
|
||||
}
|
||||
const state = {
|
||||
list: [],
|
||||
mode: 'create',
|
||||
@@ -254,6 +314,17 @@ routerAdd('GET', '/manage/document-manage', function (e) {
|
||||
pendingImageFiles: [],
|
||||
pendingVideoFiles: [],
|
||||
removedAttachmentIds: [],
|
||||
dictionaries: [],
|
||||
dictionariesByName: {},
|
||||
dictionariesById: {},
|
||||
selections: {
|
||||
documentTypeSource: '',
|
||||
documentTypeValues: [],
|
||||
documentKeywords: [],
|
||||
productCategories: [],
|
||||
applicationScenarios: [],
|
||||
hotelType: [],
|
||||
},
|
||||
}
|
||||
|
||||
function setStatus(message, type) {
|
||||
@@ -275,6 +346,46 @@ routerAdd('GET', '/manage/document-manage', function (e) {
|
||||
return match ? match[0] : ''
|
||||
}
|
||||
|
||||
function getTodayDateString() {
|
||||
const now = new Date()
|
||||
const year = now.getFullYear()
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(now.getDate()).padStart(2, '0')
|
||||
return year + '-' + month + '-' + day
|
||||
}
|
||||
|
||||
function calculateDocumentStatus(effectDate, expiryDate, fallbackStatus) {
|
||||
const start = toDateInputValue(effectDate)
|
||||
const end = toDateInputValue(expiryDate)
|
||||
const today = getTodayDateString()
|
||||
|
||||
if (!start && !end) {
|
||||
return fallbackStatus === '过期' ? '过期' : '有效'
|
||||
}
|
||||
if (start && today < start) {
|
||||
return '过期'
|
||||
}
|
||||
if (end && today > end) {
|
||||
return '过期'
|
||||
}
|
||||
return '有效'
|
||||
}
|
||||
|
||||
function updateDocumentStatusSwitch() {
|
||||
const buttons = documentStatusSwitchEl ? documentStatusSwitchEl.querySelectorAll('[data-status-value]') : []
|
||||
const currentValue = fields.documentStatus.value || '有效'
|
||||
for (let i = 0; i < buttons.length; i += 1) {
|
||||
const button = buttons[i]
|
||||
const isActive = button.getAttribute('data-status-value') === currentValue
|
||||
button.classList.toggle('active', isActive)
|
||||
}
|
||||
}
|
||||
|
||||
function syncDocumentStatus(preferredStatus) {
|
||||
fields.documentStatus.value = calculateDocumentStatus(fields.effectDate.value, fields.expiryDate.value, preferredStatus || fields.documentStatus.value || '有效')
|
||||
updateDocumentStatusSwitch()
|
||||
}
|
||||
|
||||
function normalizeAttachmentList(attachments, ids, urls) {
|
||||
const attachmentArray = Array.isArray(attachments) ? attachments : []
|
||||
if (attachmentArray.length) {
|
||||
@@ -304,6 +415,154 @@ routerAdd('GET', '/manage/document-manage', function (e) {
|
||||
})
|
||||
}
|
||||
|
||||
function splitPipeValue(value) {
|
||||
return String(value || '')
|
||||
.split('|')
|
||||
.map(function (item) { return String(item || '').trim() })
|
||||
.filter(function (item) { return !!item })
|
||||
}
|
||||
|
||||
function joinPipeValue(values) {
|
||||
return Array.from(new Set((values || []).map(function (item) {
|
||||
return String(item || '').trim()
|
||||
}).filter(function (item) {
|
||||
return !!item
|
||||
}))).join('|')
|
||||
}
|
||||
|
||||
function findDictionaryByName(dictName) {
|
||||
return state.dictionariesByName[dictName] || null
|
||||
}
|
||||
|
||||
function getDocumentTypeSourceDictionary() {
|
||||
const sourceId = String(state.selections.documentTypeSource || '')
|
||||
return sourceId ? (state.dictionariesById[sourceId] || null) : null
|
||||
}
|
||||
|
||||
function buildDocumentTypeStorageValue() {
|
||||
const sourceDict = getDocumentTypeSourceDictionary()
|
||||
if (!sourceDict) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return joinPipeValue(state.selections.documentTypeValues.map(function (enumValue) {
|
||||
return sourceDict.system_dict_id + '@' + enumValue
|
||||
}))
|
||||
}
|
||||
|
||||
function updateSelection(fieldName, value, checked) {
|
||||
const target = state.selections[fieldName] || []
|
||||
const next = target.filter(function (item) {
|
||||
return item !== value
|
||||
})
|
||||
|
||||
if (checked) {
|
||||
next.push(value)
|
||||
}
|
||||
|
||||
state.selections[fieldName] = next
|
||||
renderDictionarySelectors()
|
||||
}
|
||||
|
||||
function renderSelectionTags(tagsEl, values, labelsMap) {
|
||||
if (!values.length) {
|
||||
tagsEl.innerHTML = '<span class="muted">未选择</span>'
|
||||
return
|
||||
}
|
||||
|
||||
tagsEl.innerHTML = values.map(function (value) {
|
||||
return '<span class="selection-tag">' + escapeHtml(labelsMap[value] || value) + '</span>'
|
||||
}).join('')
|
||||
}
|
||||
|
||||
function formatDocumentTypeDisplay(value) {
|
||||
const parts = splitPipeValue(value)
|
||||
if (!parts.length) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return parts.map(function (part) {
|
||||
const separatorIndex = part.indexOf('@')
|
||||
if (separatorIndex === -1) {
|
||||
return part
|
||||
}
|
||||
|
||||
const sourceId = part.slice(0, separatorIndex)
|
||||
const enumValue = part.slice(separatorIndex + 1)
|
||||
const dict = state.dictionariesById[sourceId]
|
||||
if (!dict || !Array.isArray(dict.items)) {
|
||||
return enumValue
|
||||
}
|
||||
|
||||
const matched = dict.items.find(function (item) {
|
||||
return String(item.enum || '') === enumValue
|
||||
})
|
||||
|
||||
return matched ? (matched.description || matched.enum || enumValue) : enumValue
|
||||
}).join(' | ')
|
||||
}
|
||||
|
||||
function renderOptionList(optionsEl, selectedValues, items, fieldName) {
|
||||
if (!items || !items.length) {
|
||||
optionsEl.innerHTML = '<div class="muted">暂无可选项</div>'
|
||||
return {}
|
||||
}
|
||||
|
||||
const labelsMap = {}
|
||||
optionsEl.innerHTML = items.map(function (item) {
|
||||
const optionValue = String(item.enum || '')
|
||||
const checked = selectedValues.indexOf(optionValue) !== -1
|
||||
labelsMap[optionValue] = item.description || item.enum || optionValue
|
||||
return '<label class="option-item">'
|
||||
+ '<input type="checkbox" data-selection-field="' + fieldName + '" value="' + escapeHtml(optionValue) + '"' + (checked ? ' checked' : '') + ' />'
|
||||
+ '<span>' + escapeHtml(item.description || item.enum || optionValue) + '</span>'
|
||||
+ '</label>'
|
||||
}).join('')
|
||||
|
||||
return labelsMap
|
||||
}
|
||||
|
||||
function renderDocumentTypeSourceOptions() {
|
||||
const currentValue = String(state.selections.documentTypeSource || '')
|
||||
documentTypeSourceEl.innerHTML = ['<option value="">请选择数据来源字典</option>']
|
||||
.concat(state.dictionaries.map(function (dict) {
|
||||
return '<option value="' + escapeHtml(dict.system_dict_id) + '"' + (currentValue === dict.system_dict_id ? ' selected' : '') + '>' + escapeHtml(dict.dict_name) + '</option>'
|
||||
}))
|
||||
.join('')
|
||||
}
|
||||
|
||||
function renderDictionarySelectors() {
|
||||
renderDocumentTypeSourceOptions()
|
||||
|
||||
const sourceDict = getDocumentTypeSourceDictionary()
|
||||
const sourceItems = sourceDict && Array.isArray(sourceDict.items) ? sourceDict.items : []
|
||||
const documentTypeLabels = renderOptionList(documentTypeOptionsEl, state.selections.documentTypeValues, sourceItems, 'documentTypeValues')
|
||||
renderSelectionTags(documentTypeTagsEl, state.selections.documentTypeValues, documentTypeLabels)
|
||||
|
||||
Object.keys(dictionaryFieldConfig).forEach(function (fieldName) {
|
||||
const config = dictionaryFieldConfig[fieldName]
|
||||
const dict = findDictionaryByName(config.dictName)
|
||||
const items = dict && Array.isArray(dict.items) ? dict.items : []
|
||||
const labelsMap = renderOptionList(config.optionsEl, state.selections[fieldName], items, fieldName)
|
||||
renderSelectionTags(config.tagsEl, state.selections[fieldName], labelsMap)
|
||||
})
|
||||
}
|
||||
|
||||
async function loadDictionaries() {
|
||||
const data = await requestJson('/dictionary/list', {})
|
||||
state.dictionaries = Array.isArray(data.items) ? data.items : []
|
||||
state.dictionariesByName = {}
|
||||
state.dictionariesById = {}
|
||||
|
||||
for (let i = 0; i < state.dictionaries.length; i += 1) {
|
||||
const item = state.dictionaries[i]
|
||||
state.dictionariesByName[item.dict_name] = item
|
||||
state.dictionariesById[item.system_dict_id] = item
|
||||
}
|
||||
|
||||
renderDictionarySelectors()
|
||||
}
|
||||
|
||||
function updateEditorMode() {
|
||||
if (state.mode === 'edit') {
|
||||
formTitleEl.textContent = '编辑文档'
|
||||
@@ -345,6 +604,36 @@ routerAdd('GET', '/manage/document-manage', function (e) {
|
||||
return data.data
|
||||
}
|
||||
|
||||
async function parseJsonSafe(res) {
|
||||
const contentType = String(res.headers.get('content-type') || '').toLowerCase()
|
||||
const rawText = await res.text()
|
||||
const isJson = contentType.indexOf('application/json') !== -1
|
||||
|
||||
if (!rawText) {
|
||||
return {
|
||||
json: null,
|
||||
text: '',
|
||||
isJson: false,
|
||||
}
|
||||
}
|
||||
|
||||
if (isJson) {
|
||||
try {
|
||||
return {
|
||||
json: JSON.parse(rawText),
|
||||
text: rawText,
|
||||
isJson: true,
|
||||
}
|
||||
} catch (_error) {}
|
||||
}
|
||||
|
||||
return {
|
||||
json: null,
|
||||
text: rawText,
|
||||
isJson: false,
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadAttachment(file, label) {
|
||||
const token = getToken()
|
||||
if (!token) {
|
||||
@@ -368,8 +657,17 @@ routerAdd('GET', '/manage/document-manage', function (e) {
|
||||
body: form,
|
||||
})
|
||||
|
||||
const data = await res.json()
|
||||
const parsed = await parseJsonSafe(res)
|
||||
const data = parsed.json
|
||||
if (!res.ok || !data || data.code >= 400) {
|
||||
if (res.status === 413) {
|
||||
throw new Error('上传' + label + '失败:文件已超过当前网关允许的请求体大小。当前附件字段已放宽到约 4GB,但线上反向代理也需要同步放开到相应体积。')
|
||||
}
|
||||
|
||||
if (!parsed.isJson && parsed.text) {
|
||||
throw new Error('上传' + label + '失败:服务端返回了非 JSON 响应,通常表示网关或反向代理提前拦截了上传请求。')
|
||||
}
|
||||
|
||||
throw new Error((data && data.msg) || ('上传' + label + '失败'))
|
||||
}
|
||||
|
||||
@@ -451,15 +749,14 @@ routerAdd('GET', '/manage/document-manage', function (e) {
|
||||
|
||||
function renderTable() {
|
||||
if (!state.list.length) {
|
||||
tableBody.innerHTML = '<tr><td colspan="6" class="empty">暂无文档数据。</td></tr>'
|
||||
tableBody.innerHTML = '<tr><td colspan="5" class="empty">暂无文档数据。</td></tr>'
|
||||
return
|
||||
}
|
||||
|
||||
tableBody.innerHTML = state.list.map(function (item) {
|
||||
return '<tr>'
|
||||
+ '<td data-label="document_id"><div>' + escapeHtml(item.document_id) + '</div><div class="muted">owner: ' + escapeHtml(item.document_owner) + '</div></td>'
|
||||
+ '<td data-label="标题"><div><strong>' + escapeHtml(item.document_title) + '</strong></div><div class="muted">' + escapeHtml(item.document_subtitle) + '</div></td>'
|
||||
+ '<td data-label="类型/状态"><div>' + escapeHtml(item.document_type) + '</div><div class="muted">' + escapeHtml(item.document_status) + ' / ' + escapeHtml(item.document_embedding_status) + '</div></td>'
|
||||
+ '<td data-label="标题"><div><strong>' + escapeHtml(item.document_title) + '</strong></div><div class="muted">' + escapeHtml(item.document_subtitle) + '</div><div class="muted">owner: ' + escapeHtml(item.document_owner) + '</div></td>'
|
||||
+ '<td data-label="类型/状态"><div>' + escapeHtml(formatDocumentTypeDisplay(item.document_type) || item.document_type) + '</div><div class="muted">' + escapeHtml(item.document_status) + ' / ' + escapeHtml(item.document_embedding_status) + '</div></td>'
|
||||
+ '<td data-label="附件链接">' + renderLinks(item) + '</td>'
|
||||
+ '<td data-label="更新时间"><span class="muted">' + escapeHtml(item.updated) + '</span></td>'
|
||||
+ '<td data-label="操作"><div class="table-actions">'
|
||||
@@ -555,8 +852,7 @@ routerAdd('GET', '/manage/document-manage', function (e) {
|
||||
|
||||
function resetForm() {
|
||||
fields.documentTitle.value = ''
|
||||
fields.documentType.value = ''
|
||||
fields.documentStatus.value = ''
|
||||
fields.documentStatus.value = '有效'
|
||||
fields.embeddingStatus.value = ''
|
||||
fields.effectDate.value = ''
|
||||
fields.expiryDate.value = ''
|
||||
@@ -565,13 +861,17 @@ routerAdd('GET', '/manage/document-manage', function (e) {
|
||||
fields.documentContent.value = ''
|
||||
fields.relationModel.value = ''
|
||||
fields.vectorVersion.value = ''
|
||||
fields.documentKeywords.value = ''
|
||||
fields.productCategories.value = ''
|
||||
fields.applicationScenarios.value = ''
|
||||
fields.hotelType.value = ''
|
||||
fields.documentRemark.value = ''
|
||||
fields.imageFile.value = ''
|
||||
fields.videoFile.value = ''
|
||||
state.selections.documentTypeSource = ''
|
||||
state.selections.documentTypeValues = []
|
||||
state.selections.documentKeywords = []
|
||||
state.selections.productCategories = []
|
||||
state.selections.applicationScenarios = []
|
||||
state.selections.hotelType = []
|
||||
renderDictionarySelectors()
|
||||
syncDocumentStatus('有效')
|
||||
}
|
||||
|
||||
function enterCreateMode() {
|
||||
@@ -590,8 +890,7 @@ routerAdd('GET', '/manage/document-manage', function (e) {
|
||||
|
||||
function fillFormFromItem(item) {
|
||||
fields.documentTitle.value = item.document_title || ''
|
||||
fields.documentType.value = item.document_type || ''
|
||||
fields.documentStatus.value = item.document_status || ''
|
||||
fields.documentStatus.value = item.document_status || '有效'
|
||||
fields.embeddingStatus.value = item.document_embedding_status || ''
|
||||
fields.effectDate.value = toDateInputValue(item.document_effect_date)
|
||||
fields.expiryDate.value = toDateInputValue(item.document_expiry_date)
|
||||
@@ -600,13 +899,27 @@ routerAdd('GET', '/manage/document-manage', function (e) {
|
||||
fields.documentContent.value = item.document_content || ''
|
||||
fields.relationModel.value = item.document_relation_model || ''
|
||||
fields.vectorVersion.value = item.document_vector_version || ''
|
||||
fields.documentKeywords.value = item.document_keywords || ''
|
||||
fields.productCategories.value = item.document_product_categories || ''
|
||||
fields.applicationScenarios.value = item.document_application_scenarios || ''
|
||||
fields.hotelType.value = item.document_hotel_type || ''
|
||||
fields.documentRemark.value = item.document_remark || ''
|
||||
fields.imageFile.value = ''
|
||||
fields.videoFile.value = ''
|
||||
|
||||
const documentTypeParts = splitPipeValue(item.document_type)
|
||||
const firstDocumentType = documentTypeParts.length ? documentTypeParts[0] : ''
|
||||
const sourceId = firstDocumentType.indexOf('@') !== -1 ? firstDocumentType.split('@')[0] : ''
|
||||
state.selections.documentTypeSource = sourceId
|
||||
state.selections.documentTypeValues = documentTypeParts
|
||||
.map(function (part) {
|
||||
return part.indexOf('@') !== -1 ? part.split('@').slice(1).join('@') : ''
|
||||
})
|
||||
.filter(function (part) {
|
||||
return !!part
|
||||
})
|
||||
state.selections.documentKeywords = splitPipeValue(item.document_keywords)
|
||||
state.selections.productCategories = splitPipeValue(item.document_product_categories)
|
||||
state.selections.applicationScenarios = splitPipeValue(item.document_application_scenarios)
|
||||
state.selections.hotelType = splitPipeValue(item.document_hotel_type)
|
||||
renderDictionarySelectors()
|
||||
syncDocumentStatus(item.document_status || '有效')
|
||||
}
|
||||
|
||||
function enterEditMode(documentId) {
|
||||
@@ -639,8 +952,8 @@ routerAdd('GET', '/manage/document-manage', function (e) {
|
||||
return {
|
||||
document_id: state.mode === 'edit' ? state.editingId : '',
|
||||
document_title: fields.documentTitle.value.trim(),
|
||||
document_type: fields.documentType.value.trim(),
|
||||
document_status: fields.documentStatus.value.trim(),
|
||||
document_type: buildDocumentTypeStorageValue(),
|
||||
document_status: fields.documentStatus.value.trim() || '有效',
|
||||
document_embedding_status: fields.embeddingStatus.value.trim(),
|
||||
document_effect_date: fields.effectDate.value,
|
||||
document_expiry_date: fields.expiryDate.value,
|
||||
@@ -650,16 +963,16 @@ routerAdd('GET', '/manage/document-manage', function (e) {
|
||||
document_image: imageAttachments.map(function (item) { return item.attachments_id }),
|
||||
document_video: videoAttachments.map(function (item) { return item.attachments_id }),
|
||||
document_relation_model: fields.relationModel.value.trim(),
|
||||
document_keywords: fields.documentKeywords.value.trim(),
|
||||
document_keywords: joinPipeValue(state.selections.documentKeywords),
|
||||
document_share_count: typeof source.document_share_count === 'undefined' || source.document_share_count === null ? '' : source.document_share_count,
|
||||
document_download_count: typeof source.document_download_count === 'undefined' || source.document_download_count === null ? '' : source.document_download_count,
|
||||
document_favorite_count: typeof source.document_favorite_count === 'undefined' || source.document_favorite_count === null ? '' : source.document_favorite_count,
|
||||
document_embedding_error: source.document_embedding_error || '',
|
||||
document_embedding_lasttime: source.document_embedding_lasttime || '',
|
||||
document_vector_version: fields.vectorVersion.value.trim(),
|
||||
document_product_categories: fields.productCategories.value.trim(),
|
||||
document_application_scenarios: fields.applicationScenarios.value.trim(),
|
||||
document_hotel_type: fields.hotelType.value.trim(),
|
||||
document_product_categories: joinPipeValue(state.selections.productCategories),
|
||||
document_application_scenarios: joinPipeValue(state.selections.applicationScenarios),
|
||||
document_hotel_type: joinPipeValue(state.selections.hotelType),
|
||||
document_remark: fields.documentRemark.value.trim(),
|
||||
}
|
||||
}
|
||||
@@ -685,12 +998,14 @@ routerAdd('GET', '/manage/document-manage', function (e) {
|
||||
}
|
||||
|
||||
async function submitDocument() {
|
||||
syncDocumentStatus()
|
||||
|
||||
if (!fields.documentTitle.value.trim()) {
|
||||
setStatus('请先填写文档标题。', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
if (!fields.documentType.value.trim()) {
|
||||
if (!buildDocumentTypeStorageValue()) {
|
||||
setStatus('请先填写文档类型。', 'error')
|
||||
return
|
||||
}
|
||||
@@ -760,6 +1075,39 @@ routerAdd('GET', '/manage/document-manage', function (e) {
|
||||
window.__removePendingAttachment = removePendingAttachment
|
||||
window.__removeCurrentAttachment = removeCurrentAttachment
|
||||
|
||||
documentTypeSourceEl.addEventListener('change', function (event) {
|
||||
state.selections.documentTypeSource = event.target.value || ''
|
||||
state.selections.documentTypeValues = []
|
||||
renderDictionarySelectors()
|
||||
})
|
||||
documentStatusSwitchEl.addEventListener('click', function (event) {
|
||||
const target = event.target
|
||||
if (!target || !target.getAttribute) {
|
||||
return
|
||||
}
|
||||
|
||||
const nextValue = target.getAttribute('data-status-value')
|
||||
if (!nextValue) {
|
||||
return
|
||||
}
|
||||
|
||||
fields.documentStatus.value = nextValue
|
||||
syncDocumentStatus(nextValue)
|
||||
})
|
||||
document.addEventListener('change', function (event) {
|
||||
const target = event.target
|
||||
if (!target || !target.getAttribute) {
|
||||
return
|
||||
}
|
||||
|
||||
const fieldName = target.getAttribute('data-selection-field')
|
||||
if (!fieldName) {
|
||||
return
|
||||
}
|
||||
|
||||
updateSelection(fieldName, target.value, !!target.checked)
|
||||
})
|
||||
|
||||
document.getElementById('reloadBtn').addEventListener('click', loadDocuments)
|
||||
document.getElementById('createModeBtn').addEventListener('click', function () {
|
||||
enterCreateMode()
|
||||
@@ -790,6 +1138,12 @@ routerAdd('GET', '/manage/document-manage', function (e) {
|
||||
appendPendingFiles('image', event.target.files)
|
||||
fields.imageFile.value = ''
|
||||
})
|
||||
fields.effectDate.addEventListener('change', function () {
|
||||
syncDocumentStatus()
|
||||
})
|
||||
fields.expiryDate.addEventListener('change', function () {
|
||||
syncDocumentStatus()
|
||||
})
|
||||
fields.videoFile.addEventListener('change', function (event) {
|
||||
appendPendingFiles('video', event.target.files)
|
||||
fields.videoFile.value = ''
|
||||
@@ -798,7 +1152,14 @@ routerAdd('GET', '/manage/document-manage', function (e) {
|
||||
bindDropzone(videoDropzoneEl, fields.videoFile, 'video')
|
||||
|
||||
enterCreateMode()
|
||||
loadDocuments()
|
||||
;(async function initPage() {
|
||||
try {
|
||||
await loadDictionaries()
|
||||
} catch (err) {
|
||||
setStatus(err.message || '加载字典选项失败', 'error')
|
||||
}
|
||||
await loadDocuments()
|
||||
})()
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
@@ -16,14 +16,14 @@ routerAdd('GET', '/manage', function (e) {
|
||||
</script>
|
||||
<style>
|
||||
body { margin: 0; font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; background: linear-gradient(180deg, #f3f6fb 0%, #eef2ff 100%); color: #1f2937; }
|
||||
.wrap { max-width: 760px; margin: 0 auto; padding: 48px 20px; }
|
||||
.hero { background: #ffffff; border-radius: 24px; box-shadow: 0 18px 50px rgba(15, 23, 42, 0.08); padding: 36px; border: 1px solid #e5e7eb; }
|
||||
h1 { margin: 0 0 20px; font-size: 32px; }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 20px; }
|
||||
.card { background: #f8fafc; border: 1px solid #dbe3f0; border-radius: 18px; padding: 22px; text-align: center; }
|
||||
.card h2 { margin: 0 0 14px; font-size: 20px; }
|
||||
.wrap { max-width: 1440px; margin: 0 auto; padding: 24px 14px; }
|
||||
.hero { background: #ffffff; border-radius: 20px; box-shadow: 0 18px 50px rgba(15, 23, 42, 0.08); padding: 22px; border: 1px solid #e5e7eb; }
|
||||
h1 { margin: 0 0 14px; font-size: 30px; }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; }
|
||||
.card { background: #f8fafc; border: 1px solid #dbe3f0; border-radius: 16px; padding: 16px; text-align: center; }
|
||||
.card h2 { margin: 0 0 8px; font-size: 19px; }
|
||||
.btn { display: inline-flex; align-items: center; justify-content: center; padding: 10px 16px; border-radius: 12px; text-decoration: none; background: #2563eb; color: #fff; font-weight: 600; margin-top: 12px; }
|
||||
.actions { margin-top: 24px; display: flex; justify-content: flex-start; }
|
||||
.actions { margin-top: 14px; display: flex; justify-content: flex-start; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -22,15 +22,17 @@ function renderLoginPage(e) {
|
||||
color: #1f2937;
|
||||
}
|
||||
.wrap {
|
||||
max-width: 420px;
|
||||
max-width: 1440px;
|
||||
margin: 0 auto;
|
||||
padding: 72px 20px;
|
||||
padding: 34px 14px;
|
||||
}
|
||||
.card {
|
||||
max-width: 420px;
|
||||
margin: 0 auto;
|
||||
background: #fff;
|
||||
border: 1px solid #dbe3f0;
|
||||
border-radius: 18px;
|
||||
padding: 28px;
|
||||
padding: 22px;
|
||||
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
h1 { margin-top: 0; }
|
||||
|
||||
@@ -204,7 +204,7 @@
|
||||
|
||||
- `POST /pb/api/dictionary/list`
|
||||
- 支持按 `dict_name` 模糊搜索
|
||||
- 返回字典全量信息,并将 `dict_word_enum`、`dict_word_description`、`dict_word_sort_order` 组装为 `items`
|
||||
- 返回字典全量信息,并将 `dict_word_enum`、`dict_word_description`、`dict_word_image`、`dict_word_sort_order` 组装为 `items`
|
||||
- `POST /pb/api/dictionary/detail`
|
||||
- 按 `dict_name` 查询单条字典
|
||||
- `POST /pb/api/dictionary/create`
|
||||
@@ -216,8 +216,9 @@
|
||||
|
||||
说明:
|
||||
|
||||
- `dict_word_enum`、`dict_word_description`、`dict_word_sort_order` 已统一改为 JSON 字符串持久化,其中 `dict_word_sort_order` 已改为 `text` 类型。
|
||||
- 查询时统一聚合为:`items: [{ enum, description, sortOrder }]`
|
||||
- `dict_word_enum`、`dict_word_description`、`dict_word_image`、`dict_word_sort_order` 已统一改为 JSON 字符串持久化,其中 `dict_word_sort_order` 已改为 `text` 类型。
|
||||
- 字典项图片需先调用 `/pb/api/attachment/upload` 上传,再把返回的 `attachments_id` 写入 `dict_word_image` 对应位置。
|
||||
- 查询时统一聚合为:`items: [{ enum, description, image, imageUrl, imageAttachment, sortOrder }]`
|
||||
|
||||
### 附件管理接口
|
||||
|
||||
@@ -267,11 +268,16 @@
|
||||
- `document_id` 可不传,由服务端自动生成
|
||||
- `document_title`、`document_type` 为必填;其余字段均允许为空
|
||||
- `document_image`、`document_video` 支持传入多个已存在的 `attachments_id`
|
||||
- `document_type` 前端从单个字典来源中多选枚举值,最终按 `system_dict_id@dict_word_enum|...` 保存
|
||||
- `document_keywords`、`document_product_categories`、`document_application_scenarios`、`document_hotel_type` 统一从固定字典多选并按 `|` 保存
|
||||
- 其中 `document_product_categories` 改为从 `文档-产品关联文档` 读取,`document_application_scenarios` 改为从 `文档-筛选依据` 读取,`document_hotel_type` 改为从 `文档-适用场景` 读取
|
||||
- `document_status` 仅保留 `有效` / `过期` 两种状态,并由生效日期与到期日期自动计算
|
||||
- 成功后会写入一条文档操作历史,类型为 `create`
|
||||
- `POST /pb/api/document/update`
|
||||
- 按 `document_id` 更新文档
|
||||
- `document_title`、`document_type` 为必填;其余字段均允许为空
|
||||
- 若传入附件字段,则会校验多个 `attachments_id` 是否都存在
|
||||
- 多选字段的持久化格式与新增接口一致
|
||||
- 成功后会写入一条文档操作历史,类型为 `update`
|
||||
- `POST /pb/api/document/delete`
|
||||
- 按 `document_id` 真删除文档
|
||||
@@ -325,6 +331,8 @@
|
||||
- 指定字典查询
|
||||
- 行内编辑基础字段
|
||||
- 弹窗编辑枚举项
|
||||
- 为每个枚举项单独上传图片,并保存对应 `attachments_id`
|
||||
- 回显字典项图片缩略图与文件流链接
|
||||
- 新增 / 删除字典
|
||||
- 返回主页
|
||||
- 文档管理页支持:
|
||||
|
||||
1408
pocket-base/spec/openapi-manage.yaml
Normal file
1408
pocket-base/spec/openapi-manage.yaml
Normal file
File diff suppressed because it is too large
Load Diff
419
pocket-base/spec/openapi-wx.yaml
Normal file
419
pocket-base/spec/openapi-wx.yaml
Normal file
@@ -0,0 +1,419 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: BAI PocketBase WeChat API
|
||||
version: 1.0.0-wx
|
||||
description: |
|
||||
面向微信端的小程序接口文档。
|
||||
本文档只包含微信登录、微信资料完善,以及微信端会共用的系统接口。
|
||||
license:
|
||||
name: Proprietary
|
||||
identifier: LicenseRef-Proprietary
|
||||
servers:
|
||||
- url: https://bai-api.blv-oa.com
|
||||
description: 生产环境
|
||||
- url: http://localhost:8090
|
||||
description: PocketBase 本地环境
|
||||
tags:
|
||||
- name: 系统
|
||||
description: 微信端共用系统接口
|
||||
- name: 微信认证
|
||||
description: 微信登录、资料完善与 token 刷新接口
|
||||
paths:
|
||||
/pb/api/system/users-count:
|
||||
post:
|
||||
operationId: postSystemUsersCount
|
||||
security: []
|
||||
tags:
|
||||
- 系统
|
||||
summary: 查询用户总数
|
||||
description: 统计 `tbl_auth_users` 集合中的记录总数。
|
||||
responses:
|
||||
'200':
|
||||
description: 查询成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UsersCountResponse'
|
||||
'400':
|
||||
description: 请求参数错误
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'500':
|
||||
description: 服务端错误
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
/pb/api/system/refresh-token:
|
||||
post:
|
||||
operationId: postSystemRefreshToken
|
||||
security:
|
||||
- bearerAuth: []
|
||||
- {}
|
||||
tags:
|
||||
- 系统
|
||||
summary: 刷新认证 token
|
||||
description: |
|
||||
当当前 `Authorization` 仍有效时,直接基于当前 auth 用户续签。
|
||||
当 token 失效时,可传入 `users_wx_code` 走微信 code 重新签发。
|
||||
requestBody:
|
||||
required: false
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SystemRefreshTokenRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: 刷新成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RefreshTokenResponse'
|
||||
'400':
|
||||
description: 参数错误或微信 code 换取失败
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'401':
|
||||
description: token 无效,且未提供有效的 `users_wx_code`
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'404':
|
||||
description: 当前用户不存在
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'415':
|
||||
description: 请求体不是 JSON
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'429':
|
||||
description: 请求过于频繁
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'500':
|
||||
description: 服务端错误
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
/pb/api/wechat/login:
|
||||
post:
|
||||
operationId: postWechatLogin
|
||||
security: []
|
||||
tags:
|
||||
- 微信认证
|
||||
summary: 微信登录或首次注册
|
||||
description: |
|
||||
使用微信 `users_wx_code` 换取微信 openid。
|
||||
若 `tbl_auth_users` 中不存在对应用户,则自动创建新 auth 用户并返回 token。
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/WechatLoginRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: 登录或注册成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AuthSuccessResponse'
|
||||
'400':
|
||||
description: 参数错误或保存用户失败
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'401':
|
||||
description: 认证失败
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'415':
|
||||
description: 请求体不是 JSON
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'429':
|
||||
description: 请求过于频繁
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'500':
|
||||
description: 服务端错误
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
/pb/api/wechat/profile:
|
||||
post:
|
||||
operationId: postWechatProfile
|
||||
security:
|
||||
- bearerAuth: []
|
||||
tags:
|
||||
- 微信认证
|
||||
summary: 更新微信用户资料
|
||||
description: |
|
||||
基于当前 `Authorization` 对应的 auth 用户更新昵称、手机号和头像。
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/WechatProfileRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: 更新成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/WechatProfileResponse'
|
||||
'400':
|
||||
description: 参数错误、手机号已被占用或资料更新失败
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'401':
|
||||
description: token 无效或缺少 openid
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'415':
|
||||
description: 请求体不是 JSON
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'429':
|
||||
description: 请求过于频繁
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'500':
|
||||
description: 服务端错误
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
schemas:
|
||||
ApiResponseBase:
|
||||
type: object
|
||||
required:
|
||||
- code
|
||||
- msg
|
||||
- data
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
example: 200
|
||||
msg:
|
||||
type: string
|
||||
example: 操作成功
|
||||
data:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
ErrorResponse:
|
||||
type: object
|
||||
required:
|
||||
- code
|
||||
- msg
|
||||
- data
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
example: 400
|
||||
msg:
|
||||
type: string
|
||||
example: 请求失败
|
||||
data:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
CompanyInfo:
|
||||
anyOf:
|
||||
- type: object
|
||||
additionalProperties: true
|
||||
- type: 'null'
|
||||
UserInfo:
|
||||
type: object
|
||||
properties:
|
||||
pb_id:
|
||||
type: string
|
||||
users_convers_id:
|
||||
type: string
|
||||
users_id:
|
||||
type: string
|
||||
users_idtype:
|
||||
type: string
|
||||
enum:
|
||||
- WeChat
|
||||
- ManagePlatform
|
||||
users_id_number:
|
||||
type: string
|
||||
users_type:
|
||||
type: string
|
||||
users_name:
|
||||
type: string
|
||||
users_status:
|
||||
type:
|
||||
- string
|
||||
- number
|
||||
users_rank_level:
|
||||
type:
|
||||
- number
|
||||
- integer
|
||||
users_auth_type:
|
||||
type:
|
||||
- number
|
||||
- integer
|
||||
users_phone:
|
||||
type: string
|
||||
users_phone_masked:
|
||||
type: string
|
||||
users_level:
|
||||
type: string
|
||||
users_picture:
|
||||
type: string
|
||||
openid:
|
||||
type: string
|
||||
description: 全平台统一身份标识
|
||||
company_id:
|
||||
type: string
|
||||
users_parent_id:
|
||||
type: string
|
||||
users_promo_code:
|
||||
type: string
|
||||
usergroups_id:
|
||||
type: string
|
||||
company:
|
||||
$ref: '#/components/schemas/CompanyInfo'
|
||||
created:
|
||||
type: string
|
||||
updated:
|
||||
type: string
|
||||
WechatLoginRequest:
|
||||
type: object
|
||||
required:
|
||||
- users_wx_code
|
||||
properties:
|
||||
users_wx_code:
|
||||
type: string
|
||||
description: 微信小程序登录临时凭证 code
|
||||
example: 0a1b2c3d4e5f6g
|
||||
WechatProfileRequest:
|
||||
type: object
|
||||
required:
|
||||
- users_name
|
||||
- users_phone_code
|
||||
- users_picture
|
||||
properties:
|
||||
users_name:
|
||||
type: string
|
||||
example: 张三
|
||||
users_phone_code:
|
||||
type: string
|
||||
example: 2b7d9f2e3c4a5b6d7e8f
|
||||
users_picture:
|
||||
type: string
|
||||
example: https://example.com/avatar.png
|
||||
SystemRefreshTokenRequest:
|
||||
type: object
|
||||
properties:
|
||||
users_wx_code:
|
||||
type:
|
||||
- string
|
||||
- 'null'
|
||||
description: |
|
||||
可选。
|
||||
当前 token 失效时,可通过该 code 重新签发 token。
|
||||
example: 0a1b2c3d4e5f6g
|
||||
AuthSuccessData:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
enum:
|
||||
- register_success
|
||||
- login_success
|
||||
is_info_complete:
|
||||
type: boolean
|
||||
user:
|
||||
$ref: '#/components/schemas/UserInfo'
|
||||
AuthSuccessResponse:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/ApiResponseBase'
|
||||
- type: object
|
||||
required:
|
||||
- token
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/AuthSuccessData'
|
||||
token:
|
||||
type: string
|
||||
description: PocketBase 原生 auth token
|
||||
WechatProfileResponseData:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
enum:
|
||||
- update_success
|
||||
user:
|
||||
$ref: '#/components/schemas/UserInfo'
|
||||
WechatProfileResponse:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/ApiResponseBase'
|
||||
- type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/WechatProfileResponseData'
|
||||
RefreshTokenResponse:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/ApiResponseBase'
|
||||
- type: object
|
||||
required:
|
||||
- token
|
||||
properties:
|
||||
data:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
example: {}
|
||||
token:
|
||||
type: string
|
||||
description: 新签发的 PocketBase 原生 auth token
|
||||
UsersCountData:
|
||||
type: object
|
||||
properties:
|
||||
total_users:
|
||||
type: integer
|
||||
example: 128
|
||||
UsersCountResponse:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/ApiResponseBase'
|
||||
- type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/UsersCountData'
|
||||
@@ -292,6 +292,17 @@ components:
|
||||
description:
|
||||
type: string
|
||||
example: 启用
|
||||
image:
|
||||
type: string
|
||||
description: 对应图片附件的 `attachments_id`,允许为空
|
||||
example: ATT-1743037200000-abc123
|
||||
imageUrl:
|
||||
type: string
|
||||
description: 根据 `image -> tbl_attachments` 自动解析出的图片文件流链接
|
||||
imageAttachment:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/AttachmentRecord'
|
||||
nullable: true
|
||||
sortOrder:
|
||||
type: integer
|
||||
example: 1
|
||||
@@ -355,6 +366,7 @@ components:
|
||||
items:
|
||||
type: array
|
||||
minItems: 1
|
||||
description: 每项会分别序列化写入 `dict_word_enum`、`dict_word_description`、`dict_word_image`、`dict_word_sort_order`
|
||||
items:
|
||||
$ref: '#/components/schemas/DictionaryItem'
|
||||
DictionaryDeleteRequest:
|
||||
@@ -457,6 +469,7 @@ components:
|
||||
type: string
|
||||
document_type:
|
||||
type: string
|
||||
description: 多选时按 `system_dict_id@dict_word_enum|...` 保存
|
||||
document_title:
|
||||
type: string
|
||||
document_subtitle:
|
||||
@@ -520,7 +533,7 @@ components:
|
||||
type: string
|
||||
document_keywords:
|
||||
type: string
|
||||
description: 多值字段,使用 `|` 分隔
|
||||
description: 固定字典多选字段,使用 `|` 分隔
|
||||
document_share_count:
|
||||
type: number
|
||||
document_download_count:
|
||||
@@ -529,6 +542,7 @@ components:
|
||||
type: number
|
||||
document_status:
|
||||
type: string
|
||||
description: 文档状态,仅允许 `有效` 或 `过期`,由系统根据生效日期和到期日期自动计算;当两者都为空时默认 `有效`
|
||||
document_embedding_status:
|
||||
type: string
|
||||
document_embedding_error:
|
||||
@@ -539,13 +553,13 @@ components:
|
||||
type: string
|
||||
document_product_categories:
|
||||
type: string
|
||||
description: 多值字段,使用 `|` 分隔
|
||||
description: 固定字典多选字段,使用 `|` 分隔
|
||||
document_application_scenarios:
|
||||
type: string
|
||||
description: 多值字段,使用 `|` 分隔
|
||||
description: 固定字典多选字段,使用 `|` 分隔
|
||||
document_hotel_type:
|
||||
type: string
|
||||
description: 多值字段,使用 `|` 分隔
|
||||
description: 固定字典多选字段,使用 `|` 分隔
|
||||
document_remark:
|
||||
type: string
|
||||
created:
|
||||
@@ -564,6 +578,7 @@ components:
|
||||
example: active
|
||||
document_type:
|
||||
type: string
|
||||
description: 支持按存储值过滤;多选时格式为 `system_dict_id@dict_word_enum|...`
|
||||
example: 说明书
|
||||
DocumentDetailRequest:
|
||||
type: object
|
||||
@@ -590,6 +605,7 @@ components:
|
||||
example: 2027-03-27
|
||||
document_type:
|
||||
type: string
|
||||
description: 必填;前端显示为字典项描述,存库时按 `system_dict_id@dict_word_enum|...` 保存
|
||||
document_title:
|
||||
type: string
|
||||
document_subtitle:
|
||||
@@ -618,7 +634,7 @@ components:
|
||||
type: string
|
||||
document_keywords:
|
||||
type: string
|
||||
description: 多值字段,使用 `|` 分隔
|
||||
description: 从 `文档-关键词` 字典多选后使用 `|` 分隔保存
|
||||
document_share_count:
|
||||
type: number
|
||||
document_download_count:
|
||||
@@ -627,6 +643,7 @@ components:
|
||||
type: number
|
||||
document_status:
|
||||
type: string
|
||||
description: 文档状态,仅允许 `有效` 或 `过期`,由系统根据生效日期和到期日期自动计算;当两者都为空时默认 `有效`
|
||||
document_embedding_status:
|
||||
type: string
|
||||
document_embedding_error:
|
||||
@@ -637,13 +654,13 @@ components:
|
||||
type: string
|
||||
document_product_categories:
|
||||
type: string
|
||||
description: 多值字段,使用 `|` 分隔
|
||||
description: 从 `文档-产品关联文档` 字典多选后使用 `|` 分隔保存
|
||||
document_application_scenarios:
|
||||
type: string
|
||||
description: 多值字段,使用 `|` 分隔
|
||||
description: 从 `文档-筛选依据` 字典多选后使用 `|` 分隔保存
|
||||
document_hotel_type:
|
||||
type: string
|
||||
description: 多值字段,使用 `|` 分隔
|
||||
description: 从 `文档-适用场景` 字典多选后使用 `|` 分隔保存
|
||||
document_remark:
|
||||
type: string
|
||||
DocumentDeleteRequest:
|
||||
@@ -1005,7 +1022,7 @@ paths:
|
||||
description: |
|
||||
仅允许 `ManagePlatform` 用户访问。
|
||||
`system_dict_id` 由服务端自动生成;`dict_name` 必须唯一;
|
||||
`items` 会分别序列化写入 `dict_word_enum`、`dict_word_description`、`dict_word_sort_order` 三个字段。
|
||||
`items` 会分别序列化写入 `dict_word_enum`、`dict_word_description`、`dict_word_image`、`dict_word_sort_order` 四个字段。
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
|
||||
Reference in New Issue
Block a user