feat: 添加 SDK 权限管理页面和产品功能字段迁移脚本
- 新增 SDK 权限管理页面,包含角色管理、用户授权和集合权限配置功能。 - 实现字段迁移脚本,向 tbl_product_list 集合添加 prod_list_function 字段,类型为 json。
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
| `users_phone` | `手机号 | string` |
|
||||
| `users_phone_masked` | `脱敏手机号 | string` |
|
||||
| `users_level` | `用户等级 | string` |
|
||||
| `users_level_name` | `用户等级名称 | string` |
|
||||
| `users_tag` | `用户标签 | string` |
|
||||
| `users_picture` | `用户头像附件ID | string` |
|
||||
| `users_picture_url` | `用户头像文件流链接 | string` |
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
| `users_idtype` | `text` | 否 | 身份来源类型或证件类型 |
|
||||
| `users_id_number` | `text` | 否 | 证件号 |
|
||||
| `users_phone` | `text` | 否 | 手机号 |
|
||||
| `users_level` | `text` | 否 | 用户等级文本 |
|
||||
| `users_level` | `text` | 否 | 用户等级枚举值,保存“数据-会员等级”字典 enum |
|
||||
| `users_type` | `text` | 否 | 用户类型 |
|
||||
| `company_id` | `text` | 否 | 所属公司业务 ID |
|
||||
| `users_parent_id` | `text` | 否 | 上级用户业务 ID |
|
||||
@@ -63,4 +63,5 @@
|
||||
- 本表为 `auth` collection,除上述字段外还受 PocketBase 原生鉴权机制约束。
|
||||
- 图片类字段统一只保存 `tbl_attachments.attachments_id`。
|
||||
- 登录接口返回的 token 来源于本表 auth record 的原生签发能力,可直接给 PocketBase SDK 使用。
|
||||
- 新用户注册时,`users_level` 默认保持为空;已有用户后续登录 / 更新流程也不会自动改写该字段。
|
||||
- PocketBase 系统字段 `created`、`updated` 仍然存在,只是不在 collection 字段清单里单独声明。
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
| `prod_list_tags` | `text` | 否 | 产品标签(辅助检索,以 `|` 聚合) |
|
||||
| `prod_list_status` | `text` | 否 | 产品状态(有效 / 过期 / 主推等) |
|
||||
| `prod_list_basic_price` | `number` | 否 | 基础价格 |
|
||||
| `prod_list_vip_price` | `json` | 否 | 会员价数组,格式为 `[{"viplevel":"会员等级枚举值","price":1999}]` |
|
||||
| `prod_list_remark` | `text` | 否 | 备注 |
|
||||
|
||||
## 索引
|
||||
@@ -51,6 +52,8 @@
|
||||
- 当前预构建脚本中已将 `listRule` 与 `viewRule` 设置为空字符串(`""`),对应 PocketBase 的“任何人可查看”。
|
||||
- `prod_list_parameters` 使用 PocketBase `json` 字段,写入时应直接提交数组结构:`[{"sort":1,"name":"属性名","value":"属性值"}]`。
|
||||
- `prod_list_parameters.sort` 用于稳定参数展示顺序,约定为正整数;前端未填写时可按当前录入/导入顺序自动补齐。
|
||||
- `prod_list_vip_price` 使用 PocketBase `json` 字段,写入时应直接提交数组结构:`[{"viplevel":"VIP1","price":1999}]`。
|
||||
- `prod_list_vip_price.viplevel` 必须保存“数据-会员等级”字典中的枚举值,`price` 保存对应会员等级价格。
|
||||
- `prod_list_description` 作为 editor 内容字段,建议统一为 Markdown 或净化后的 HTML;若允许 HTML,需在写入前做 XSS 白名单过滤。
|
||||
- 前端渲染 `prod_list_description` 时应保持和存储格式一致(Markdown 渲染器或受控 HTML 渲染),避免直接注入未净化内容。
|
||||
- `prod_list_basic_price` 使用 `number`,如需货币精度策略建议后续统一为“分”或固定小数位。
|
||||
|
||||
@@ -52,6 +52,7 @@ require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/order/create.js`)
|
||||
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/order/update.js`)
|
||||
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/order/delete.js`)
|
||||
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/cart-order/manage-users.js`)
|
||||
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/cart-order/manage-user-update.js`)
|
||||
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/document-history/list.js`)
|
||||
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/sdk-permission/context.js`)
|
||||
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/sdk-permission/role-save.js`)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
routerAdd('POST', '/api/cart-order/manage-user-update', function (e) {
|
||||
const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`)
|
||||
const cartOrderService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/cartOrderService.js`)
|
||||
const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`)
|
||||
const { success, fail } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/response.js`)
|
||||
|
||||
try {
|
||||
guards.requireJson(e)
|
||||
guards.requireManagePlatformUser(e)
|
||||
const payload = guards.validateCartOrderManageUserUpdateBody(e)
|
||||
const data = cartOrderService.updateManageUser(payload)
|
||||
|
||||
return success(e, '更新用户信息成功', {
|
||||
user: data,
|
||||
})
|
||||
} catch (err) {
|
||||
const status = (err && err.statusCode) || (err && err.status) || 400
|
||||
logger.error('更新用户信息失败', {
|
||||
status: status,
|
||||
errMsg: (err && err.message) || '未知错误',
|
||||
data: (err && err.data) || {},
|
||||
})
|
||||
return fail(e, (err && err.message) || '操作失败', (err && err.data) || {}, status)
|
||||
}
|
||||
})
|
||||
@@ -10,12 +10,10 @@ routerAdd('POST', '/api/cart-order/manage-users', function (e) {
|
||||
const payload = guards.validateCartOrderManageListBody(e)
|
||||
const data = cartOrderService.listManageUsersCartOrders(payload)
|
||||
|
||||
return success(e, '查询用户购物车与订单成功', {
|
||||
items: data,
|
||||
})
|
||||
return success(e, '查询用户信息、购物车与订单成功', data)
|
||||
} catch (err) {
|
||||
const status = (err && err.statusCode) || (err && err.status) || 400
|
||||
logger.error('查询用户购物车与订单失败', {
|
||||
logger.error('查询用户信息、购物车与订单失败', {
|
||||
status: status,
|
||||
errMsg: (err && err.message) || '未知错误',
|
||||
data: (err && err.data) || {},
|
||||
|
||||
@@ -1,15 +1,28 @@
|
||||
routerAdd('POST', '/api/sdk-permission/context', function (e) {
|
||||
const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`)
|
||||
const permissionService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/sdkPermissionService.js`)
|
||||
const { success } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/response.js`)
|
||||
const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`)
|
||||
const { success, fail } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/response.js`)
|
||||
|
||||
guards.requireJson(e)
|
||||
guards.requireManagePlatformUser(e)
|
||||
try {
|
||||
guards.requireJson(e)
|
||||
guards.requireManagePlatformUser(e)
|
||||
|
||||
const payload = guards.validateSdkPermissionContextBody(e)
|
||||
const data = permissionService.getManagementContext(payload.keyword)
|
||||
const payload = guards.validateSdkPermissionContextBody(e)
|
||||
const data = permissionService.getManagementContext(payload.keyword)
|
||||
|
||||
return success(e, '查询权限管理上下文成功', data)
|
||||
return success(e, '查询权限管理上下文成功', data)
|
||||
} catch (err) {
|
||||
const status = (err && err.statusCode) || (err && err.status) || 400
|
||||
|
||||
logger.error('查询权限管理上下文失败', {
|
||||
status: status,
|
||||
errMsg: (err && err.message) || '未知错误',
|
||||
data: (err && err.data) || {},
|
||||
})
|
||||
|
||||
return fail(e, (err && err.message) || '操作失败', (err && err.data) || {}, status)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
NODE_ENV=production
|
||||
APP_BASE_URL=https://bai-api.blv-oa.com
|
||||
POCKETBASE_API_URL=https://bai-api.blv-oa.com/pb/
|
||||
POCKETBASE_API_URL=http://127.0.0.1:8090
|
||||
POCKETBASE_AUTH_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJwYmNfMzE0MjYzNTgyMyIsImV4cCI6MTgwNTk2MzM4NywiaWQiOiJmcnl2dG1qNnlwcHlmMXAiLCJyZWZyZXNoYWJsZSI6ZmFsc2UsInR5cGUiOiJhdXRoIn0.R34MTotuFBpevqbbUtvQ3GPa0Z1mpY8eQwZgDdmfhMo
|
||||
|
||||
#正式服
|
||||
|
||||
@@ -2,7 +2,7 @@ module.exports = {
|
||||
NODE_ENV: 'production',
|
||||
APP_VERSION: '0.1.21',
|
||||
APP_BASE_URL: 'https://bai-api.blv-oa.com',
|
||||
POCKETBASE_API_URL: 'https://bai-api.blv-oa.com/pb',
|
||||
POCKETBASE_API_URL: 'http://127.0.0.1:8090',
|
||||
POCKETBASE_AUTH_TOKEN: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJwYmNfMzE0MjYzNTgyMyIsImV4cCI6MTgwNTk2MzM4NywiaWQiOiJmcnl2dG1qNnlwcHlmMXAiLCJyZWZyZXNoYWJsZSI6ZmFsc2UsInR5cGUiOiJhdXRoIn0.R34MTotuFBpevqbbUtvQ3GPa0Z1mpY8eQwZgDdmfhMo',
|
||||
WECHAT_APPID: 'wx3bd7a7b19679da7a',
|
||||
WECHAT_SECRET: '57e40438c2a9151257b1927674db10e1',
|
||||
|
||||
@@ -107,14 +107,32 @@ function validateSystemRefreshBody(e) {
|
||||
}
|
||||
|
||||
function requireManagePlatformUser(e) {
|
||||
const authUser = requireAuthUser(e)
|
||||
const idType = authUser.authRecord.getString('users_idtype')
|
||||
const authHeader = e.request.header.get('Authorization') || ''
|
||||
const parts = authHeader.split(' ')
|
||||
|
||||
if (parts.length !== 2 || parts[0] !== 'Bearer' || !parts[1]) {
|
||||
throw createAppError(401, '请求头缺少 Authorization')
|
||||
}
|
||||
|
||||
if (!e.auth) {
|
||||
throw createAppError(401, '认证令牌无效或已过期')
|
||||
}
|
||||
|
||||
if (e.auth.collection().name !== 'tbl_auth_users') {
|
||||
throw createAppError(401, '认证用户集合不正确')
|
||||
}
|
||||
|
||||
const authRecord = e.auth
|
||||
const idType = authRecord.getString('users_idtype')
|
||||
|
||||
if (idType !== 'ManagePlatform') {
|
||||
throw createAppError(403, '仅平台管理用户可访问')
|
||||
}
|
||||
|
||||
return authUser
|
||||
return {
|
||||
openid: authRecord.getString('openid') || '',
|
||||
authRecord: authRecord,
|
||||
}
|
||||
}
|
||||
|
||||
function validateDictionaryListBody(e) {
|
||||
@@ -415,6 +433,57 @@ function normalizeProductParameters(value) {
|
||||
return result
|
||||
}
|
||||
|
||||
function normalizeProductVipPrice(value) {
|
||||
if (value === null || typeof value === 'undefined' || value === '') {
|
||||
return []
|
||||
}
|
||||
|
||||
let source = value
|
||||
if (typeof source === 'string') {
|
||||
try {
|
||||
source = JSON.parse(source)
|
||||
} catch (_error) {
|
||||
throw createAppError(400, 'prod_list_vip_price 必须为合法 JSON 数组')
|
||||
}
|
||||
}
|
||||
|
||||
if (!Array.isArray(source)) {
|
||||
throw createAppError(400, 'prod_list_vip_price 必须为数组')
|
||||
}
|
||||
|
||||
const result = []
|
||||
const levelMap = {}
|
||||
|
||||
for (let i = 0; i < source.length; i += 1) {
|
||||
const item = source[i] && typeof source[i] === 'object' ? source[i] : null
|
||||
if (!item) {
|
||||
continue
|
||||
}
|
||||
|
||||
const viplevel = String(item.viplevel || '').trim()
|
||||
if (!viplevel) {
|
||||
throw createAppError(400, 'prod_list_vip_price 第 ' + (i + 1) + ' 项 viplevel 为必填项')
|
||||
}
|
||||
|
||||
const price = Number(item.price)
|
||||
if (!Number.isFinite(price)) {
|
||||
throw createAppError(400, 'prod_list_vip_price 第 ' + (i + 1) + ' 项 price 必须为数字')
|
||||
}
|
||||
|
||||
if (levelMap[viplevel]) {
|
||||
throw createAppError(400, 'prod_list_vip_price 中 viplevel 不允许重复:' + viplevel)
|
||||
}
|
||||
|
||||
levelMap[viplevel] = true
|
||||
result.push({
|
||||
viplevel: viplevel,
|
||||
price: price,
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function validateProductListBody(e) {
|
||||
const payload = parseBody(e)
|
||||
|
||||
@@ -459,6 +528,7 @@ function validateProductMutationBody(e, isUpdate) {
|
||||
prod_list_description: payload.prod_list_description || '',
|
||||
prod_list_feature: payload.prod_list_feature || '',
|
||||
prod_list_parameters: normalizeProductParameters(payload.prod_list_parameters),
|
||||
prod_list_function: normalizeProductParameters(payload.prod_list_function),
|
||||
prod_list_plantype: payload.prod_list_plantype || '',
|
||||
prod_list_category: payload.prod_list_category || '',
|
||||
prod_list_sort: typeof payload.prod_list_sort === 'undefined' ? 0 : payload.prod_list_sort,
|
||||
@@ -468,6 +538,7 @@ function validateProductMutationBody(e, isUpdate) {
|
||||
prod_list_tags: payload.prod_list_tags || '',
|
||||
prod_list_status: payload.prod_list_status || '',
|
||||
prod_list_basic_price: typeof payload.prod_list_basic_price === 'undefined' ? '' : payload.prod_list_basic_price,
|
||||
prod_list_vip_price: normalizeProductVipPrice(payload.prod_list_vip_price),
|
||||
prod_list_remark: payload.prod_list_remark || '',
|
||||
}
|
||||
}
|
||||
@@ -599,6 +670,34 @@ function validateCartOrderManageListBody(e) {
|
||||
}
|
||||
}
|
||||
|
||||
function validateCartOrderManageUserUpdateBody(e) {
|
||||
const payload = parseBody(e)
|
||||
if (!payload.openid) {
|
||||
throw createAppError(400, 'openid 为必填项')
|
||||
}
|
||||
|
||||
return {
|
||||
openid: String(payload.openid || '').trim(),
|
||||
users_name: Object.prototype.hasOwnProperty.call(payload, 'users_name') ? payload.users_name : undefined,
|
||||
users_phone: Object.prototype.hasOwnProperty.call(payload, 'users_phone') ? payload.users_phone : undefined,
|
||||
users_id_number: Object.prototype.hasOwnProperty.call(payload, 'users_id_number') ? payload.users_id_number : undefined,
|
||||
users_level: Object.prototype.hasOwnProperty.call(payload, 'users_level') ? payload.users_level : undefined,
|
||||
users_type: Object.prototype.hasOwnProperty.call(payload, 'users_type') ? payload.users_type : undefined,
|
||||
users_tag: Object.prototype.hasOwnProperty.call(payload, 'users_tag') ? payload.users_tag : undefined,
|
||||
users_status: Object.prototype.hasOwnProperty.call(payload, 'users_status') ? payload.users_status : undefined,
|
||||
users_rank_level: Object.prototype.hasOwnProperty.call(payload, 'users_rank_level') ? payload.users_rank_level : undefined,
|
||||
users_auth_type: Object.prototype.hasOwnProperty.call(payload, 'users_auth_type') ? payload.users_auth_type : undefined,
|
||||
company_id: Object.prototype.hasOwnProperty.call(payload, 'company_id') ? payload.company_id : undefined,
|
||||
users_parent_id: Object.prototype.hasOwnProperty.call(payload, 'users_parent_id') ? payload.users_parent_id : undefined,
|
||||
users_promo_code: Object.prototype.hasOwnProperty.call(payload, 'users_promo_code') ? payload.users_promo_code : undefined,
|
||||
usergroups_id: Object.prototype.hasOwnProperty.call(payload, 'usergroups_id') ? payload.usergroups_id : undefined,
|
||||
users_picture: Object.prototype.hasOwnProperty.call(payload, 'users_picture') ? payload.users_picture : undefined,
|
||||
users_id_pic_a: Object.prototype.hasOwnProperty.call(payload, 'users_id_pic_a') ? payload.users_id_pic_a : undefined,
|
||||
users_id_pic_b: Object.prototype.hasOwnProperty.call(payload, 'users_id_pic_b') ? payload.users_id_pic_b : undefined,
|
||||
users_title_picture: Object.prototype.hasOwnProperty.call(payload, 'users_title_picture') ? payload.users_title_picture : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
function validateDocumentHistoryListBody(e) {
|
||||
const payload = parseBody(e)
|
||||
|
||||
@@ -783,6 +882,7 @@ module.exports = {
|
||||
validateOrderMutationBody,
|
||||
validateOrderDeleteBody,
|
||||
validateCartOrderManageListBody,
|
||||
validateCartOrderManageUserUpdateBody,
|
||||
validateDocumentHistoryListBody,
|
||||
validateSdkPermissionContextBody,
|
||||
validateSdkPermissionRoleBody,
|
||||
|
||||
@@ -1,10 +1,79 @@
|
||||
const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`)
|
||||
const { createAppError } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/appError.js`)
|
||||
const env = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/config/env.js`)
|
||||
const userService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/userService.js`)
|
||||
|
||||
function buildBusinessId(prefix) {
|
||||
return prefix + '-' + new Date().getTime() + '-' + $security.randomString(6)
|
||||
}
|
||||
|
||||
function parseJsonResponse(response, actionName) {
|
||||
if (!response) {
|
||||
throw createAppError(500, actionName + ' 失败:PocketBase 响应为空')
|
||||
}
|
||||
|
||||
if (response.json && typeof response.json === 'object') {
|
||||
return response.json
|
||||
}
|
||||
|
||||
if (typeof response.body === 'string' && response.body) {
|
||||
return JSON.parse(response.body)
|
||||
}
|
||||
|
||||
if (response.body && typeof response.body === 'object') {
|
||||
return response.body
|
||||
}
|
||||
|
||||
if (typeof response.data === 'string' && response.data) {
|
||||
return JSON.parse(response.data)
|
||||
}
|
||||
|
||||
if (response.data && typeof response.data === 'object') {
|
||||
return response.data
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
|
||||
function getPocketBaseApiBaseUrl() {
|
||||
const base = String(env.pocketbaseApiUrl || '').replace(/\/+$/, '')
|
||||
if (!base) {
|
||||
throw createAppError(500, '缺少 POCKETBASE_API_URL 配置')
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
function getPocketBaseAuthToken() {
|
||||
const token = String(env.pocketbaseAuthToken || '').trim()
|
||||
if (!token) {
|
||||
throw createAppError(500, '缺少 POCKETBASE_AUTH_TOKEN 配置')
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
function requestPocketBase(method, path, body, actionName) {
|
||||
const response = $http.send({
|
||||
url: getPocketBaseApiBaseUrl() + path,
|
||||
method: method,
|
||||
headers: body ? {
|
||||
Authorization: 'Bearer ' + getPocketBaseAuthToken(),
|
||||
'Content-Type': 'application/json',
|
||||
} : {
|
||||
Authorization: 'Bearer ' + getPocketBaseAuthToken(),
|
||||
},
|
||||
body: body ? JSON.stringify(body) : '',
|
||||
})
|
||||
|
||||
if (!response || response.statusCode < 200 || response.statusCode >= 300) {
|
||||
throw createAppError(500, actionName + ' 失败', {
|
||||
statusCode: response ? response.statusCode : 0,
|
||||
body: response ? String(response.body || '') : '',
|
||||
})
|
||||
}
|
||||
|
||||
return parseJsonResponse(response, actionName)
|
||||
}
|
||||
|
||||
function normalizeText(value) {
|
||||
return String(value || '').replace(/^\s+|\s+$/g, '')
|
||||
}
|
||||
@@ -234,6 +303,98 @@ function exportOrderRecord(record) {
|
||||
}
|
||||
}
|
||||
|
||||
function exportAdminCartRecord(record) {
|
||||
const productInfo = buildProductInfo(findProductRecordByBusinessId(record.cart_product_id))
|
||||
|
||||
return {
|
||||
pb_id: String(record.id || ''),
|
||||
cart_id: String(record.cart_id || ''),
|
||||
cart_number: String(record.cart_number || ''),
|
||||
cart_create: String(record.cart_create || ''),
|
||||
cart_owner: String(record.cart_owner || ''),
|
||||
cart_product_id: String(record.cart_product_id || ''),
|
||||
cart_product_quantity: Number(record.cart_product_quantity || 0),
|
||||
cart_status: String(record.cart_status || ''),
|
||||
cart_at_price: Number(record.cart_at_price || 0),
|
||||
cart_remark: String(record.cart_remark || ''),
|
||||
product_name: productInfo.prod_list_name,
|
||||
product_modelnumber: productInfo.prod_list_modelnumber,
|
||||
product_basic_price: productInfo.prod_list_basic_price,
|
||||
created: String(record.created || ''),
|
||||
updated: String(record.updated || ''),
|
||||
}
|
||||
}
|
||||
|
||||
function exportAdminOrderRecord(record) {
|
||||
return {
|
||||
pb_id: String(record.id || ''),
|
||||
order_id: String(record.order_id || ''),
|
||||
order_number: String(record.order_number || ''),
|
||||
order_create: String(record.order_create || ''),
|
||||
order_owner: String(record.order_owner || ''),
|
||||
order_source: String(record.order_source || ''),
|
||||
order_status: String(record.order_status || ''),
|
||||
order_source_id: String(record.order_source_id || ''),
|
||||
order_snap: parseJsonFieldForOutput(record.order_snap),
|
||||
order_amount: Number(record.order_amount || 0),
|
||||
order_remark: String(record.order_remark || ''),
|
||||
created: String(record.created || ''),
|
||||
updated: String(record.updated || ''),
|
||||
}
|
||||
}
|
||||
|
||||
function exportAdminManageUser(userRecord, groupedCarts, groupedOrders) {
|
||||
const openid = String(userRecord.openid || '')
|
||||
const carts = groupedCarts[openid] || []
|
||||
const orders = groupedOrders[openid] || []
|
||||
let cartTotalQuantity = 0
|
||||
let orderTotalAmount = 0
|
||||
|
||||
for (let i = 0; i < carts.length; i += 1) {
|
||||
cartTotalQuantity += Number(carts[i].cart_product_quantity || 0)
|
||||
}
|
||||
for (let i = 0; i < orders.length; i += 1) {
|
||||
orderTotalAmount += Number(orders[i].order_amount || 0)
|
||||
}
|
||||
|
||||
return {
|
||||
pb_id: String(userRecord.id || ''),
|
||||
openid: openid,
|
||||
users_id: String(userRecord.users_id || ''),
|
||||
users_name: String(userRecord.users_name || ''),
|
||||
users_phone: String(userRecord.users_phone || ''),
|
||||
users_level: String(userRecord.users_level || ''),
|
||||
users_level_name: userService.resolveUserLevelName(String(userRecord.users_level || '')),
|
||||
users_type: String(userRecord.users_type || ''),
|
||||
users_idtype: String(userRecord.users_idtype || ''),
|
||||
users_id_number: String(userRecord.users_id_number || ''),
|
||||
users_status: String(userRecord.users_status || ''),
|
||||
users_rank_level: userRecord.users_rank_level === null || typeof userRecord.users_rank_level === 'undefined'
|
||||
? null
|
||||
: Number(userRecord.users_rank_level),
|
||||
users_auth_type: userRecord.users_auth_type === null || typeof userRecord.users_auth_type === 'undefined'
|
||||
? null
|
||||
: Number(userRecord.users_auth_type),
|
||||
users_tag: String(userRecord.users_tag || ''),
|
||||
company_id: String(userRecord.company_id || ''),
|
||||
users_parent_id: String(userRecord.users_parent_id || ''),
|
||||
users_promo_code: String(userRecord.users_promo_code || ''),
|
||||
usergroups_id: String(userRecord.usergroups_id || ''),
|
||||
users_picture: String(userRecord.users_picture || ''),
|
||||
users_id_pic_a: String(userRecord.users_id_pic_a || ''),
|
||||
users_id_pic_b: String(userRecord.users_id_pic_b || ''),
|
||||
users_title_picture: String(userRecord.users_title_picture || ''),
|
||||
cart_count: carts.length,
|
||||
cart_total_quantity: cartTotalQuantity,
|
||||
order_count: orders.length,
|
||||
order_total_amount: orderTotalAmount,
|
||||
carts: carts,
|
||||
orders: orders,
|
||||
created: String(userRecord.created || ''),
|
||||
updated: String(userRecord.updated || ''),
|
||||
}
|
||||
}
|
||||
|
||||
function listCarts(authOpenid, payload) {
|
||||
const records = $app.findRecordsByFilter('tbl_cart', 'cart_owner = {:owner}', '-cart_create', 500, 0, {
|
||||
owner: authOpenid,
|
||||
@@ -560,31 +721,31 @@ function exportManageUser(userRecord, groupedCarts, groupedOrders) {
|
||||
|
||||
function listManageUsersCartOrders(payload) {
|
||||
const keyword = normalizeText(payload.keyword).toLowerCase()
|
||||
const userRecords = $app.findRecordsByFilter('tbl_auth_users', '', '-created', 500, 0)
|
||||
const userRecords = $app.findRecordsByFilter('tbl_auth_users', '', '-users_id', 500, 0)
|
||||
const cartRecords = $app.findRecordsByFilter('tbl_cart', '', '-cart_create', 1000, 0)
|
||||
const orderRecords = $app.findRecordsByFilter('tbl_order', '', '-order_create', 1000, 0)
|
||||
const groupedCarts = {}
|
||||
const groupedOrders = {}
|
||||
|
||||
for (let i = 0; i < cartRecords.length; i += 1) {
|
||||
const owner = cartRecords[i].getString('cart_owner')
|
||||
const owner = String(cartRecords[i].cart_owner || '')
|
||||
if (!groupedCarts[owner]) {
|
||||
groupedCarts[owner] = []
|
||||
}
|
||||
groupedCarts[owner].push(exportCartRecord(cartRecords[i]))
|
||||
groupedCarts[owner].push(exportAdminCartRecord(cartRecords[i]))
|
||||
}
|
||||
|
||||
for (let i = 0; i < orderRecords.length; i += 1) {
|
||||
const owner = orderRecords[i].getString('order_owner')
|
||||
const owner = String(orderRecords[i].order_owner || '')
|
||||
if (!groupedOrders[owner]) {
|
||||
groupedOrders[owner] = []
|
||||
}
|
||||
groupedOrders[owner].push(exportOrderRecord(orderRecords[i]))
|
||||
groupedOrders[owner].push(exportAdminOrderRecord(orderRecords[i]))
|
||||
}
|
||||
|
||||
const result = []
|
||||
for (let i = 0; i < userRecords.length; i += 1) {
|
||||
const item = exportManageUser(userRecords[i], groupedCarts, groupedOrders)
|
||||
const item = exportAdminManageUser(userRecords[i], groupedCarts, groupedOrders)
|
||||
const matchedKeyword = !keyword
|
||||
|| normalizeText(item.openid).toLowerCase().indexOf(keyword) !== -1
|
||||
|| normalizeText(item.users_id).toLowerCase().indexOf(keyword) !== -1
|
||||
@@ -596,7 +757,14 @@ function listManageUsersCartOrders(payload) {
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
return {
|
||||
items: result,
|
||||
user_level_options: userService.getUserLevelOptions(),
|
||||
}
|
||||
}
|
||||
|
||||
function updateManageUser(payload) {
|
||||
return userService.updateManagedUserProfile(payload).user
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
@@ -611,4 +779,5 @@ module.exports = {
|
||||
updateOrder,
|
||||
deleteOrder,
|
||||
listManageUsersCartOrders,
|
||||
updateManageUser,
|
||||
}
|
||||
|
||||
@@ -99,6 +99,17 @@ function exportDictionaryRecord(record) {
|
||||
}
|
||||
}
|
||||
|
||||
function sortDictionaryItems(items) {
|
||||
return (Array.isArray(items) ? items.slice() : []).sort(function (a, b) {
|
||||
const sortDiff = Number(a.sortOrder || 0) - Number(b.sortOrder || 0)
|
||||
if (sortDiff !== 0) {
|
||||
return sortDiff
|
||||
}
|
||||
|
||||
return String(a.enum || '').localeCompare(String(b.enum || ''))
|
||||
})
|
||||
}
|
||||
|
||||
function findDictionaryByName(dictName) {
|
||||
const records = $app.findRecordsByFilter('tbl_system_dict', 'dict_name = {:dictName}', '', 1, 0, {
|
||||
dictName: dictName,
|
||||
@@ -157,6 +168,15 @@ function getDictionaryByName(dictName) {
|
||||
return exportDictionaryRecord(record)
|
||||
}
|
||||
|
||||
function getDictionaryItemsByName(dictName) {
|
||||
const dictionary = getDictionaryByName(dictName)
|
||||
if (!dictionary.dict_word_is_enabled) {
|
||||
return []
|
||||
}
|
||||
|
||||
return sortDictionaryItems(dictionary.items)
|
||||
}
|
||||
|
||||
function createDictionary(payload) {
|
||||
ensureDictionaryNameUnique(payload.dict_name)
|
||||
|
||||
@@ -245,6 +265,7 @@ function deleteDictionary(dictName) {
|
||||
module.exports = {
|
||||
listDictionaries,
|
||||
getDictionaryByName,
|
||||
getDictionaryItemsByName,
|
||||
createDictionary,
|
||||
updateDictionary,
|
||||
deleteDictionary,
|
||||
|
||||
@@ -435,7 +435,6 @@ function deleteAttachment(attachmentId) {
|
||||
|
||||
function listDocuments(payload) {
|
||||
const allRecords = $app.findRecordsByFilter('tbl_document', '', '', 500, 0)
|
||||
const keyword = String(payload.keyword || '').toLowerCase()
|
||||
const titleKeyword = String(payload.title_keyword || '').toLowerCase().trim()
|
||||
const status = String(payload.status || '')
|
||||
const type = String(payload.document_type || '')
|
||||
@@ -443,12 +442,6 @@ function listDocuments(payload) {
|
||||
|
||||
for (let i = 0; i < allRecords.length; i += 1) {
|
||||
const item = exportDocumentRecord(allRecords[i])
|
||||
const matchedKeyword = !keyword
|
||||
|| item.document_id.toLowerCase().indexOf(keyword) !== -1
|
||||
|| item.document_title.toLowerCase().indexOf(keyword) !== -1
|
||||
|| item.document_subtitle.toLowerCase().indexOf(keyword) !== -1
|
||||
|| item.document_summary.toLowerCase().indexOf(keyword) !== -1
|
||||
|| item.document_keywords.toLowerCase().indexOf(keyword) !== -1
|
||||
const matchedTitleKeyword = !titleKeyword
|
||||
|| item.document_title.toLowerCase().indexOf(titleKeyword) !== -1
|
||||
const matchedStatus = !status || item.document_status === status
|
||||
@@ -465,7 +458,7 @@ function listDocuments(payload) {
|
||||
return token.indexOf(type + '@') === 0
|
||||
})
|
||||
|
||||
if (matchedKeyword && matchedTitleKeyword && matchedStatus && matchedType) {
|
||||
if (matchedTitleKeyword && matchedStatus && matchedType) {
|
||||
result.push(item)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`)
|
||||
const { createAppError } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/appError.js`)
|
||||
const documentService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/documentService.js`)
|
||||
const dictionaryService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/dictionaryService.js`)
|
||||
|
||||
const USER_LEVEL_DICT_NAME = '数据-会员等级'
|
||||
|
||||
function buildBusinessId(prefix) {
|
||||
return prefix + '-' + new Date().getTime() + '-' + $security.randomString(6)
|
||||
@@ -126,6 +129,18 @@ function sortParameterRows(rows) {
|
||||
})
|
||||
}
|
||||
|
||||
function safeObjectKeys(source) {
|
||||
if (!source || typeof source !== 'object') {
|
||||
return []
|
||||
}
|
||||
|
||||
try {
|
||||
return Object.keys(source)
|
||||
} catch (_error) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function buildCategoryRankMap(records) {
|
||||
const grouped = {}
|
||||
for (let i = 0; i < records.length; i += 1) {
|
||||
@@ -213,7 +228,7 @@ function normalizeParameters(value) {
|
||||
throw createAppError(400, 'prod_list_parameters 必须是数组或对象')
|
||||
}
|
||||
|
||||
const keys = Object.keys(value)
|
||||
const keys = safeObjectKeys(value)
|
||||
for (let i = 0; i < keys.length; i += 1) {
|
||||
upsert(keys[i], value[keys[i]], '', i + 1, i + 1)
|
||||
}
|
||||
@@ -254,6 +269,9 @@ function normalizeParametersForOutput(value) {
|
||||
if (typeof source === 'string') {
|
||||
try {
|
||||
source = JSON.parse(source)
|
||||
if (source === null) {
|
||||
return []
|
||||
}
|
||||
} catch (_error) {
|
||||
const raw = normalizeText(source)
|
||||
if (raw.indexOf('map[') === 0 && raw.endsWith(']')) {
|
||||
@@ -300,14 +318,22 @@ function normalizeParametersForOutput(value) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (source === null) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Some PocketBase/Goja map-like values are not directly enumerable; roundtrip to plain object.
|
||||
try {
|
||||
source = JSON.parse(JSON.stringify(source))
|
||||
} catch (_error) {}
|
||||
|
||||
if (source === null || typeof source !== 'object') {
|
||||
return []
|
||||
}
|
||||
|
||||
const result = []
|
||||
const indexByName = {}
|
||||
const keys = Object.keys(source)
|
||||
const keys = safeObjectKeys(source)
|
||||
for (let i = 0; i < keys.length; i += 1) {
|
||||
pushOrUpdate(result, indexByName, keys[i], source[keys[i]], '', i + 1, i + 1)
|
||||
}
|
||||
@@ -315,6 +341,126 @@ function normalizeParametersForOutput(value) {
|
||||
return sortParameterRows(result)
|
||||
}
|
||||
|
||||
function getVipLevelValueMap() {
|
||||
let items = []
|
||||
try {
|
||||
items = dictionaryService.getDictionaryItemsByName(USER_LEVEL_DICT_NAME)
|
||||
} catch (_error) {
|
||||
items = []
|
||||
}
|
||||
const result = {}
|
||||
|
||||
for (let i = 0; i < items.length; i += 1) {
|
||||
const key = normalizeText(items[i].enum)
|
||||
if (!key) {
|
||||
continue
|
||||
}
|
||||
result[key] = {
|
||||
value: key,
|
||||
label: String(items[i].description || ''),
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function normalizeVipPriceRows(value) {
|
||||
if (value === null || typeof value === 'undefined' || value === '') {
|
||||
return []
|
||||
}
|
||||
|
||||
let source = value
|
||||
if (typeof source === 'string') {
|
||||
try {
|
||||
source = JSON.parse(source)
|
||||
} catch (_error) {
|
||||
throw createAppError(400, 'prod_list_vip_price 必须为合法 JSON 数组')
|
||||
}
|
||||
}
|
||||
|
||||
if (!Array.isArray(source)) {
|
||||
throw createAppError(400, 'prod_list_vip_price 必须为数组')
|
||||
}
|
||||
|
||||
const levelMap = getVipLevelValueMap()
|
||||
const result = []
|
||||
const duplicatedLevelMap = {}
|
||||
|
||||
for (let i = 0; i < source.length; i += 1) {
|
||||
const item = source[i] && typeof source[i] === 'object' ? source[i] : null
|
||||
if (!item) {
|
||||
continue
|
||||
}
|
||||
|
||||
const viplevel = normalizeText(item.viplevel)
|
||||
if (!viplevel) {
|
||||
throw createAppError(400, 'prod_list_vip_price 第 ' + (i + 1) + ' 项 viplevel 为必填项')
|
||||
}
|
||||
if (!levelMap[viplevel]) {
|
||||
throw createAppError(400, 'prod_list_vip_price 第 ' + (i + 1) + ' 项 viplevel 不在“数据-会员等级”字典中:' + viplevel)
|
||||
}
|
||||
|
||||
const price = Number(item.price)
|
||||
if (!Number.isFinite(price)) {
|
||||
throw createAppError(400, 'prod_list_vip_price 第 ' + (i + 1) + ' 项 price 必须为数字')
|
||||
}
|
||||
|
||||
if (duplicatedLevelMap[viplevel]) {
|
||||
throw createAppError(400, 'prod_list_vip_price 中 viplevel 不允许重复:' + viplevel)
|
||||
}
|
||||
|
||||
duplicatedLevelMap[viplevel] = true
|
||||
result.push({
|
||||
viplevel: viplevel,
|
||||
price: price,
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function normalizeVipPriceRowsForOutput(value) {
|
||||
if (value === null || typeof value === 'undefined' || value === '') {
|
||||
return []
|
||||
}
|
||||
|
||||
let source = value
|
||||
if (typeof source === 'string') {
|
||||
try {
|
||||
source = JSON.parse(source)
|
||||
} catch (_error) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
if (!Array.isArray(source)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const levelMap = getVipLevelValueMap()
|
||||
const result = []
|
||||
for (let i = 0; i < source.length; i += 1) {
|
||||
const item = source[i] && typeof source[i] === 'object' ? source[i] : null
|
||||
if (!item) {
|
||||
continue
|
||||
}
|
||||
|
||||
const viplevel = normalizeText(item.viplevel)
|
||||
const price = Number(item.price)
|
||||
if (!viplevel || !Number.isFinite(price)) {
|
||||
continue
|
||||
}
|
||||
|
||||
result.push({
|
||||
viplevel: viplevel,
|
||||
viplevel_name: levelMap[viplevel] ? levelMap[viplevel].label : '',
|
||||
price: price,
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function normalizeAttachmentIdList(value) {
|
||||
const result = []
|
||||
const items = normalizePipeValues(value)
|
||||
@@ -368,10 +514,19 @@ function exportProductRecord(record, extra) {
|
||||
const firstIconAttachment = iconAttachments.length ? iconAttachments[0] : null
|
||||
|
||||
const parametersText = normalizeText(record.getString('prod_list_parameters'))
|
||||
const parametersFromText = parametersText ? normalizeParametersForOutput(parametersText) : {}
|
||||
const parametersFromText = parametersText ? normalizeParametersForOutput(parametersText) : []
|
||||
const parametersRaw = record.get('prod_list_parameters')
|
||||
const parametersFromRaw = normalizeParametersForOutput(parametersRaw)
|
||||
const parameters = parametersFromText.length ? parametersFromText : parametersFromRaw
|
||||
const functionText = normalizeText(record.getString('prod_list_function'))
|
||||
const functionFromText = functionText ? normalizeParametersForOutput(functionText) : []
|
||||
const functionRaw = record.get('prod_list_function')
|
||||
const functionFromRaw = normalizeParametersForOutput(functionRaw)
|
||||
const functions = functionFromText.length ? functionFromText : functionFromRaw
|
||||
const vipPriceText = normalizeText(record.getString('prod_list_vip_price'))
|
||||
const vipPriceFromText = vipPriceText ? normalizeVipPriceRowsForOutput(vipPriceText) : []
|
||||
const vipPriceFromRaw = normalizeVipPriceRowsForOutput(record.get('prod_list_vip_price'))
|
||||
const vipPrice = vipPriceFromText.length ? vipPriceFromText : vipPriceFromRaw
|
||||
|
||||
return {
|
||||
pb_id: record.id,
|
||||
@@ -387,6 +542,7 @@ function exportProductRecord(record, extra) {
|
||||
prod_list_description: record.getString('prod_list_description'),
|
||||
prod_list_feature: record.getString('prod_list_feature'),
|
||||
prod_list_parameters: parameters,
|
||||
prod_list_function: functions,
|
||||
prod_list_plantype: record.getString('prod_list_plantype'),
|
||||
prod_list_category: record.getString('prod_list_category'),
|
||||
prod_list_sort: Number(record.get('prod_list_sort') || 0),
|
||||
@@ -396,6 +552,7 @@ function exportProductRecord(record, extra) {
|
||||
prod_list_power_supply: record.getString('prod_list_power_supply'),
|
||||
prod_list_tags: record.getString('prod_list_tags'),
|
||||
prod_list_basic_price: record.get('prod_list_basic_price'),
|
||||
prod_list_vip_price: vipPrice,
|
||||
prod_list_status: record.getString('prod_list_status'),
|
||||
prod_list_remark: record.getString('prod_list_remark'),
|
||||
created: String(record.created || ''),
|
||||
@@ -404,42 +561,57 @@ function exportProductRecord(record, extra) {
|
||||
}
|
||||
|
||||
function listProducts(payload) {
|
||||
const allRecords = $app.findRecordsByFilter('tbl_product_list', '', '', 500, 0)
|
||||
const keyword = normalizeText(payload.keyword).toLowerCase()
|
||||
const status = normalizeText(payload.status)
|
||||
const category = normalizeText(payload.prod_list_category)
|
||||
const result = []
|
||||
try {
|
||||
const allRecords = $app.findRecordsByFilter('tbl_product_list', '', '', 500, 0)
|
||||
const keyword = normalizeText(payload.keyword).toLowerCase()
|
||||
const status = normalizeText(payload.status)
|
||||
const category = normalizeText(payload.prod_list_category)
|
||||
const result = []
|
||||
|
||||
const allItems = []
|
||||
for (let i = 0; i < allRecords.length; i += 1) {
|
||||
allItems.push(exportProductRecord(allRecords[i]))
|
||||
}
|
||||
|
||||
const rankMap = buildCategoryRankMap(allItems)
|
||||
|
||||
for (let i = 0; i < allItems.length; i += 1) {
|
||||
const source = allItems[i]
|
||||
const item = Object.assign({}, source, {
|
||||
prod_list_category_rank: rankMap[source.prod_list_id] || 0,
|
||||
})
|
||||
const matchedKeyword = !keyword
|
||||
|| item.prod_list_id.toLowerCase().indexOf(keyword) !== -1
|
||||
|| item.prod_list_name.toLowerCase().indexOf(keyword) !== -1
|
||||
|| item.prod_list_modelnumber.toLowerCase().indexOf(keyword) !== -1
|
||||
|| item.prod_list_tags.toLowerCase().indexOf(keyword) !== -1
|
||||
const matchedStatus = !status || item.prod_list_status === status
|
||||
const matchedCategory = !category || item.prod_list_category === category
|
||||
|
||||
if (matchedKeyword && matchedStatus && matchedCategory) {
|
||||
result.push(item)
|
||||
const allItems = []
|
||||
for (let i = 0; i < allRecords.length; i += 1) {
|
||||
try {
|
||||
allItems.push(exportProductRecord(allRecords[i]))
|
||||
} catch (err) {
|
||||
logger.error('产品记录导出失败,已跳过', {
|
||||
pb_id: allRecords[i] && allRecords[i].id ? allRecords[i].id : '',
|
||||
prod_list_id: allRecords[i] ? allRecords[i].getString('prod_list_id') : '',
|
||||
errMsg: (err && err.message) || '未知错误',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const rankMap = buildCategoryRankMap(allItems)
|
||||
|
||||
for (let i = 0; i < allItems.length; i += 1) {
|
||||
const source = allItems[i]
|
||||
const item = Object.assign({}, source, {
|
||||
prod_list_category_rank: rankMap[source.prod_list_id] || 0,
|
||||
})
|
||||
const matchedKeyword = !keyword
|
||||
|| item.prod_list_id.toLowerCase().indexOf(keyword) !== -1
|
||||
|| item.prod_list_name.toLowerCase().indexOf(keyword) !== -1
|
||||
|| item.prod_list_modelnumber.toLowerCase().indexOf(keyword) !== -1
|
||||
|| item.prod_list_tags.toLowerCase().indexOf(keyword) !== -1
|
||||
const matchedStatus = !status || item.prod_list_status === status
|
||||
const matchedCategory = !category || item.prod_list_category === category
|
||||
|
||||
if (matchedKeyword && matchedStatus && matchedCategory) {
|
||||
result.push(item)
|
||||
}
|
||||
}
|
||||
|
||||
result.sort(function (a, b) {
|
||||
return String(b.updated || '').localeCompare(String(a.updated || ''))
|
||||
})
|
||||
|
||||
return result
|
||||
} catch (err) {
|
||||
logger.error('产品列表构建失败,返回空列表兜底', {
|
||||
errMsg: (err && err.message) || '未知错误',
|
||||
})
|
||||
return []
|
||||
}
|
||||
|
||||
result.sort(function (a, b) {
|
||||
return String(b.updated || '').localeCompare(String(a.updated || ''))
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function getProductDetail(productId) {
|
||||
@@ -479,6 +651,7 @@ function createProduct(_userOpenid, payload) {
|
||||
record.set('prod_list_description', normalizeText(payload.prod_list_description))
|
||||
record.set('prod_list_feature', normalizeText(payload.prod_list_feature))
|
||||
record.set('prod_list_parameters', normalizeParameters(payload.prod_list_parameters))
|
||||
record.set('prod_list_function', normalizeParameters(payload.prod_list_function))
|
||||
record.set('prod_list_plantype', normalizeText(payload.prod_list_plantype))
|
||||
record.set('prod_list_category', normalizeRequiredCategory(payload.prod_list_category))
|
||||
record.set('prod_list_sort', normalizeSortValue(payload.prod_list_sort))
|
||||
@@ -488,6 +661,7 @@ function createProduct(_userOpenid, payload) {
|
||||
record.set('prod_list_tags', joinUniquePipeValues(payload.prod_list_tags))
|
||||
record.set('prod_list_status', normalizeText(payload.prod_list_status))
|
||||
record.set('prod_list_basic_price', normalizeOptionalNumberValue(payload.prod_list_basic_price, 'prod_list_basic_price'))
|
||||
record.set('prod_list_vip_price', normalizeVipPriceRows(payload.prod_list_vip_price))
|
||||
record.set('prod_list_remark', normalizeText(payload.prod_list_remark))
|
||||
|
||||
try {
|
||||
@@ -528,6 +702,7 @@ function updateProduct(_userOpenid, payload) {
|
||||
record.set('prod_list_description', normalizeText(payload.prod_list_description))
|
||||
record.set('prod_list_feature', normalizeText(payload.prod_list_feature))
|
||||
record.set('prod_list_parameters', normalizeParameters(payload.prod_list_parameters))
|
||||
record.set('prod_list_function', normalizeParameters(payload.prod_list_function))
|
||||
record.set('prod_list_plantype', normalizeText(payload.prod_list_plantype))
|
||||
record.set('prod_list_category', normalizeRequiredCategory(payload.prod_list_category))
|
||||
record.set('prod_list_sort', normalizeSortValue(payload.prod_list_sort))
|
||||
@@ -537,6 +712,7 @@ function updateProduct(_userOpenid, payload) {
|
||||
record.set('prod_list_tags', joinUniquePipeValues(payload.prod_list_tags))
|
||||
record.set('prod_list_status', normalizeText(payload.prod_list_status))
|
||||
record.set('prod_list_basic_price', normalizeOptionalNumberValue(payload.prod_list_basic_price, 'prod_list_basic_price'))
|
||||
record.set('prod_list_vip_price', normalizeVipPriceRows(payload.prod_list_vip_price))
|
||||
record.set('prod_list_remark', normalizeText(payload.prod_list_remark))
|
||||
|
||||
try {
|
||||
|
||||
@@ -100,6 +100,7 @@ function uniqueList(values) {
|
||||
|
||||
function listRoles() {
|
||||
const records = $app.findRecordsByFilter(ROLE_COLLECTION, '', 'role_name', 500, 0)
|
||||
|
||||
return records.map(function (record) {
|
||||
return {
|
||||
pb_id: record.id,
|
||||
@@ -353,7 +354,7 @@ function saveCollectionRules(payload) {
|
||||
|
||||
function listUsers(keyword, roleMap) {
|
||||
const search = normalizeText(keyword).toLowerCase()
|
||||
const records = $app.findRecordsByFilter(USER_COLLECTION, '', '', 500, 0)
|
||||
const records = $app.findRecordsByFilter(USER_COLLECTION, '', '-users_id', 500, 0)
|
||||
const items = records.map(function (record) {
|
||||
const usergroupsId = record.getString('usergroups_id')
|
||||
const role = roleMap && roleMap[usergroupsId] ? roleMap[usergroupsId] : null
|
||||
|
||||
@@ -2,12 +2,14 @@ const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.
|
||||
const { createAppError } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/appError.js`)
|
||||
const wechatService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/wechatService.js`)
|
||||
const documentService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/documentService.js`)
|
||||
const dictionaryService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/dictionaryService.js`)
|
||||
const env = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/config/env.js`)
|
||||
|
||||
const GUEST_USER_TYPE = '游客'
|
||||
const REGISTERED_USER_TYPE = '注册用户'
|
||||
const WECHAT_ID_TYPE = 'WeChat'
|
||||
const MANAGE_PLATFORM_ID_TYPE = 'ManagePlatform'
|
||||
const USER_LEVEL_DICT_NAME = '数据-会员等级'
|
||||
const mutationLocks = {}
|
||||
|
||||
function buildUserId() {
|
||||
@@ -42,6 +44,74 @@ function isAllProfileFieldsEmpty(record) {
|
||||
return !record.getString('users_name') && !record.getString('users_phone') && !record.getString('users_picture')
|
||||
}
|
||||
|
||||
function normalizeText(value) {
|
||||
return String(value || '').replace(/^\s+|\s+$/g, '')
|
||||
}
|
||||
|
||||
function normalizeOptionalNumber(value, fieldName) {
|
||||
const raw = normalizeText(value)
|
||||
if (!raw) {
|
||||
return null
|
||||
}
|
||||
|
||||
const num = Number(raw)
|
||||
if (!Number.isFinite(num)) {
|
||||
throw createAppError(400, fieldName + ' 必须为数字')
|
||||
}
|
||||
|
||||
return num
|
||||
}
|
||||
|
||||
function getUserLevelItems() {
|
||||
try {
|
||||
return dictionaryService.getDictionaryItemsByName(USER_LEVEL_DICT_NAME)
|
||||
} catch (_error) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function getUserLevelOptions() {
|
||||
return getUserLevelItems().map(function (item) {
|
||||
return {
|
||||
value: String(item.enum || ''),
|
||||
label: String(item.description || ''),
|
||||
sort: Number(item.sortOrder || 0),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function resolveUserLevelName(usersLevel) {
|
||||
const level = normalizeText(usersLevel)
|
||||
if (!level) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const items = getUserLevelItems()
|
||||
for (let i = 0; i < items.length; i += 1) {
|
||||
if (normalizeText(items[i].enum) === level) {
|
||||
return String(items[i].description || '')
|
||||
}
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
function ensureValidUserLevel(usersLevel) {
|
||||
const level = normalizeText(usersLevel)
|
||||
if (!level) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const items = getUserLevelItems()
|
||||
for (let i = 0; i < items.length; i += 1) {
|
||||
if (normalizeText(items[i].enum) === level) {
|
||||
return level
|
||||
}
|
||||
}
|
||||
|
||||
throw createAppError(400, 'users_level 不在“数据-会员等级”字典中')
|
||||
}
|
||||
|
||||
function withUserLock(lockKey, handler) {
|
||||
if (mutationLocks[lockKey]) {
|
||||
throw createAppError(429, '请求过于频繁,请稍后重试')
|
||||
@@ -275,6 +345,7 @@ function enrichUser(userRecord) {
|
||||
users_phone: userRecord.getString('users_phone'),
|
||||
users_phone_masked: maskPhone(userRecord.getString('users_phone')),
|
||||
users_level: userRecord.getString('users_level'),
|
||||
users_level_name: resolveUserLevelName(userRecord.getString('users_level')),
|
||||
users_tag: userRecord.getString('users_tag'),
|
||||
users_picture: userPicture.id,
|
||||
users_picture_url: userPicture.url,
|
||||
@@ -438,7 +509,6 @@ function registerPlatformUser(payload) {
|
||||
record.set('users_name', payload.users_name)
|
||||
record.set('users_phone', payload.users_phone)
|
||||
record.set('users_id_number', payload.users_id_number || '')
|
||||
record.set('users_level', payload.users_level || '')
|
||||
record.set('users_type', payload.users_type || GUEST_USER_TYPE)
|
||||
record.set('users_tag', payload.users_tag || '')
|
||||
record.set('company_id', payload.company_id || '')
|
||||
@@ -637,6 +707,99 @@ function updateWechatUserProfile(usersWxOpenid, payload) {
|
||||
})
|
||||
}
|
||||
|
||||
function updateManagedUserProfile(payload) {
|
||||
return withUserLock('manage-user-update:' + payload.openid, function () {
|
||||
const currentUser = findUserByOpenid(payload.openid)
|
||||
if (!currentUser) {
|
||||
throw createAppError(404, '未找到待编辑的用户')
|
||||
}
|
||||
|
||||
const nextPhone = Object.prototype.hasOwnProperty.call(payload, 'users_phone')
|
||||
? normalizeText(payload.users_phone)
|
||||
: undefined
|
||||
|
||||
if (typeof nextPhone !== 'undefined' && nextPhone && nextPhone !== currentUser.getString('users_phone')) {
|
||||
const samePhoneUsers = $app.findRecordsByFilter('tbl_auth_users', 'users_phone = {:phone}', '', 10, 0, {
|
||||
phone: nextPhone,
|
||||
})
|
||||
|
||||
for (let i = 0; i < samePhoneUsers.length; i += 1) {
|
||||
if (samePhoneUsers[i].id !== currentUser.id) {
|
||||
throw createAppError(400, '手机号已被注册')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'users_name')) {
|
||||
currentUser.set('users_name', normalizeText(payload.users_name))
|
||||
}
|
||||
if (typeof nextPhone !== 'undefined') {
|
||||
currentUser.set('users_phone', nextPhone)
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'users_id_number')) {
|
||||
currentUser.set('users_id_number', normalizeText(payload.users_id_number))
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'users_level')) {
|
||||
currentUser.set('users_level', ensureValidUserLevel(payload.users_level))
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'users_type')) {
|
||||
currentUser.set('users_type', normalizeText(payload.users_type))
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'users_tag')) {
|
||||
currentUser.set('users_tag', normalizeText(payload.users_tag))
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'company_id')) {
|
||||
currentUser.set('company_id', normalizeText(payload.company_id))
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'users_parent_id')) {
|
||||
currentUser.set('users_parent_id', normalizeText(payload.users_parent_id))
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'users_promo_code')) {
|
||||
currentUser.set('users_promo_code', normalizeText(payload.users_promo_code))
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'usergroups_id')) {
|
||||
currentUser.set('usergroups_id', normalizeText(payload.usergroups_id))
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'users_status')) {
|
||||
currentUser.set('users_status', normalizeText(payload.users_status))
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'users_rank_level')) {
|
||||
currentUser.set('users_rank_level', normalizeOptionalNumber(payload.users_rank_level, 'users_rank_level'))
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'users_auth_type')) {
|
||||
currentUser.set('users_auth_type', normalizeOptionalNumber(payload.users_auth_type, 'users_auth_type'))
|
||||
}
|
||||
|
||||
applyUserAttachmentFields(currentUser, payload)
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'users_picture') && !normalizeText(payload.users_picture)) {
|
||||
currentUser.set('users_picture', '')
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'users_id_pic_a') && !normalizeText(payload.users_id_pic_a)) {
|
||||
currentUser.set('users_id_pic_a', '')
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'users_id_pic_b') && !normalizeText(payload.users_id_pic_b)) {
|
||||
currentUser.set('users_id_pic_b', '')
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'users_title_picture') && !normalizeText(payload.users_title_picture)) {
|
||||
currentUser.set('users_title_picture', '')
|
||||
}
|
||||
|
||||
saveAuthUserRecord(currentUser)
|
||||
|
||||
const user = enrichUser(currentUser)
|
||||
|
||||
logger.info('管理端更新用户资料成功', {
|
||||
users_id: user.users_id,
|
||||
openid: user.openid,
|
||||
})
|
||||
|
||||
return {
|
||||
status: 'update_success',
|
||||
user: user,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function refreshAuthToken(openid) {
|
||||
const userRecord = findUserByOpenid(openid)
|
||||
if (!userRecord) {
|
||||
@@ -690,8 +853,11 @@ module.exports = {
|
||||
authenticatePlatformUser,
|
||||
ensureAuthToken,
|
||||
updateWechatUserProfile,
|
||||
updateManagedUserProfile,
|
||||
refreshAuthToken,
|
||||
issueAuthToken,
|
||||
registerPlatformUser,
|
||||
resolveUserLevelName,
|
||||
getUserLevelOptions,
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,11 @@ function successWithToken(e, msg, data, token, code) {
|
||||
|
||||
function fail(e, msg, data, code) {
|
||||
const meta = applyHttpMeta(e, code || 400, msg || '操作失败')
|
||||
return e.json(meta.statusCode, normalizePayloadData(data))
|
||||
const payload = normalizePayloadData(data)
|
||||
payload.statusCode = meta.statusCode
|
||||
payload.errMsg = meta.errMsg
|
||||
payload.message = meta.errMsg
|
||||
return e.json(meta.statusCode, payload)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -1,312 +1,9 @@
|
||||
routerAdd('GET', '/manage/cart-order-manage', function (e) {
|
||||
const html = `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>购物车与订单管理</title>
|
||||
<script>
|
||||
(function () {
|
||||
var token = localStorage.getItem('pb_manage_token') || ''
|
||||
var isLoggedIn = localStorage.getItem('pb_manage_logged_in') === '1'
|
||||
if (!token || !isLoggedIn) {
|
||||
window.location.replace('/pb/manage/login')
|
||||
}
|
||||
})()
|
||||
</script>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; background: linear-gradient(180deg, #eef4ff 0%, #f8fbff 100%); color: #1f2937; }
|
||||
.container { max-width: 1520px; margin: 0 auto; padding: 24px 14px 40px; }
|
||||
.panel { background: rgba(255,255,255,0.97); 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; }
|
||||
.actions { display: flex; flex-wrap: wrap; gap: 10px; }
|
||||
.toolbar { display: grid; grid-template-columns: minmax(0, 1fr) auto auto auto; gap: 10px; }
|
||||
.btn { border: none; border-radius: 12px; padding: 10px 16px; font-weight: 600; cursor: pointer; text-decoration: none; display: inline-flex; align-items: center; justify-content: center; }
|
||||
.btn-primary { background: #2563eb; color: #fff; }
|
||||
.btn-light { background: #f8fafc; color: #334155; border: 1px solid #dbe3f0; }
|
||||
.btn-danger { background: #dc2626; color: #fff; }
|
||||
.status { margin-top: 12px; min-height: 24px; font-size: 14px; }
|
||||
.status.success { color: #15803d; }
|
||||
.status.error { color: #b91c1c; }
|
||||
input { width: 100%; border: 1px solid #cbd5e1; border-radius: 12px; padding: 10px 12px; font-size: 14px; background: #fff; }
|
||||
.layout { display: grid; grid-template-columns: minmax(340px, 420px) minmax(0, 1fr); gap: 14px; align-items: start; }
|
||||
.user-list { display: grid; gap: 10px; max-height: 72vh; overflow: auto; }
|
||||
.user-card { border: 1px solid #dbe3f0; border-radius: 16px; padding: 14px; background: #f8fbff; cursor: pointer; }
|
||||
.user-card.active { border-color: #2563eb; background: #eff6ff; box-shadow: inset 0 0 0 1px #bfdbfe; }
|
||||
.user-name { font-size: 18px; font-weight: 700; color: #0f172a; }
|
||||
.user-meta { margin-top: 6px; color: #64748b; font-size: 13px; word-break: break-all; }
|
||||
.user-stats { margin-top: 10px; display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.stat-tag { display: inline-flex; align-items: center; padding: 4px 10px; border-radius: 999px; background: #dbeafe; color: #1d4ed8; font-size: 12px; font-weight: 700; }
|
||||
.detail-header { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 12px; align-items: flex-start; margin-bottom: 14px; }
|
||||
.detail-title { margin: 0; font-size: 22px; }
|
||||
.detail-meta { color: #64748b; font-size: 13px; line-height: 1.7; }
|
||||
.summary-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; margin-bottom: 14px; }
|
||||
.summary-card { border: 1px solid #dbe3f0; border-radius: 16px; padding: 14px; background: #f8fbff; }
|
||||
.summary-label { color: #64748b; font-size: 13px; }
|
||||
.summary-value { margin-top: 8px; font-size: 22px; font-weight: 700; color: #0f172a; }
|
||||
.section + .section { margin-top: 14px; }
|
||||
.section-title { margin: 0 0 10px; font-size: 18px; color: #0f172a; }
|
||||
.table-wrap { overflow: auto; border: 1px solid #dbe3f0; border-radius: 16px; background: #fff; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
thead { background: #eff6ff; }
|
||||
th, td { padding: 12px; border-bottom: 1px solid #e5e7eb; text-align: left; vertical-align: top; }
|
||||
.muted { color: #64748b; font-size: 12px; }
|
||||
.empty { text-align: center; padding: 24px; color: #64748b; }
|
||||
@media (max-width: 1080px) {
|
||||
.layout { grid-template-columns: 1fr; }
|
||||
.summary-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.toolbar { grid-template-columns: 1fr; }
|
||||
.summary-grid { grid-template-columns: 1fr; }
|
||||
table, thead, tbody, th, td, tr { display: block; }
|
||||
thead { display: none; }
|
||||
tr { border-bottom: 1px solid #e5e7eb; }
|
||||
td { display: flex; justify-content: space-between; gap: 10px; }
|
||||
td::before { content: attr(data-label); font-weight: 700; color: #475569; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<section class="panel">
|
||||
<h1 style="margin-top:0;">购物车与订单管理</h1>
|
||||
<div class="actions">
|
||||
<a class="btn btn-light" href="/pb/manage">返回主页</a>
|
||||
<button class="btn btn-light" id="reloadBtn" type="button">刷新</button>
|
||||
<button class="btn btn-danger" id="logoutBtn" type="button">退出登录</button>
|
||||
</div>
|
||||
<div class="status" id="status"></div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="toolbar">
|
||||
<input id="keywordInput" placeholder="按用户名 / openid / 手机号 / users_id 搜索" />
|
||||
<button class="btn btn-primary" id="searchBtn" type="button">搜索</button>
|
||||
<button class="btn btn-light" id="resetBtn" type="button">重置</button>
|
||||
<button class="btn btn-light" id="refreshBtn" type="button">重新加载</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="layout">
|
||||
<section class="panel">
|
||||
<h2 style="margin-top:0;">用户列表</h2>
|
||||
<div id="userList" class="user-list"></div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div id="detailWrap"></div>
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API_BASE = '/pb/api'
|
||||
const tokenKey = 'pb_manage_token'
|
||||
const state = {
|
||||
users: [],
|
||||
selectedOpenid: '',
|
||||
}
|
||||
|
||||
const statusEl = document.getElementById('status')
|
||||
const keywordInput = document.getElementById('keywordInput')
|
||||
const userListEl = document.getElementById('userList')
|
||||
const detailWrapEl = document.getElementById('detailWrap')
|
||||
|
||||
function normalizeText(value) {
|
||||
return String(value || '').trim()
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value || '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
function setStatus(message, type) {
|
||||
statusEl.textContent = message || ''
|
||||
statusEl.className = 'status' + (type ? ' ' + type : '')
|
||||
}
|
||||
|
||||
function getToken() {
|
||||
return localStorage.getItem(tokenKey) || ''
|
||||
}
|
||||
|
||||
async function requestJson(path, payload) {
|
||||
const token = getToken()
|
||||
if (!token) {
|
||||
window.location.replace('/pb/manage/login')
|
||||
throw new Error('登录状态已失效,请重新登录')
|
||||
}
|
||||
|
||||
const res = await fetch(API_BASE + path, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: 'Bearer ' + token,
|
||||
},
|
||||
body: JSON.stringify(payload || {}),
|
||||
})
|
||||
|
||||
const data = await res.json()
|
||||
if (!res.ok) {
|
||||
throw new Error((data && (data.errMsg || data.message)) || '请求失败')
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
function getSelectedUser() {
|
||||
return state.users.find(function (item) {
|
||||
return normalizeText(item.openid) === normalizeText(state.selectedOpenid)
|
||||
}) || null
|
||||
}
|
||||
|
||||
function renderUserList() {
|
||||
if (!state.users.length) {
|
||||
userListEl.innerHTML = '<div class="empty">暂无匹配用户。</div>'
|
||||
return
|
||||
}
|
||||
|
||||
userListEl.innerHTML = state.users.map(function (item) {
|
||||
const isActive = normalizeText(item.openid) === normalizeText(state.selectedOpenid)
|
||||
return '<div class="user-card' + (isActive ? ' active' : '') + '" data-openid="' + escapeHtml(item.openid) + '">'
|
||||
+ '<div class="user-name">' + escapeHtml(item.users_name || item.users_id || item.openid) + '</div>'
|
||||
+ '<div class="user-meta">openid:' + escapeHtml(item.openid || '-') + '</div>'
|
||||
+ '<div class="user-meta">手机号:' + escapeHtml(item.users_phone || '-') + '</div>'
|
||||
+ '<div class="user-meta">users_id:' + escapeHtml(item.users_id || '-') + '</div>'
|
||||
+ '<div class="user-stats">'
|
||||
+ '<span class="stat-tag">购物车 ' + escapeHtml(item.cart_count || 0) + '</span>'
|
||||
+ '<span class="stat-tag">购物数量 ' + escapeHtml(item.cart_total_quantity || 0) + '</span>'
|
||||
+ '<span class="stat-tag">订单 ' + escapeHtml(item.order_count || 0) + '</span>'
|
||||
+ '</div>'
|
||||
+ '</div>'
|
||||
}).join('')
|
||||
}
|
||||
|
||||
function renderCartTable(items) {
|
||||
if (!items.length) {
|
||||
return '<div class="empty">当前用户暂无购物车记录。</div>'
|
||||
}
|
||||
|
||||
return '<div class="table-wrap"><table><thead><tr>'
|
||||
+ '<th>商品名称</th><th>型号</th><th>数量</th><th>单价</th><th>状态</th><th>加入时间</th><th>购物车名</th>'
|
||||
+ '</tr></thead><tbody>'
|
||||
+ items.map(function (item) {
|
||||
return '<tr>'
|
||||
+ '<td data-label="商品名称"><div>' + escapeHtml(item.product_name || item.cart_product_id || '-') + '</div><div class="muted">' + escapeHtml(item.cart_product_id || '-') + '</div></td>'
|
||||
+ '<td data-label="型号">' + escapeHtml(item.product_modelnumber || '-') + '</td>'
|
||||
+ '<td data-label="数量">' + escapeHtml(item.cart_product_quantity || 0) + '</td>'
|
||||
+ '<td data-label="单价">¥' + escapeHtml(item.cart_at_price || 0) + '</td>'
|
||||
+ '<td data-label="状态">' + escapeHtml(item.cart_status || '-') + '</td>'
|
||||
+ '<td data-label="加入时间">' + escapeHtml(item.cart_create || item.created || '-') + '</td>'
|
||||
+ '<td data-label="购物车名">' + escapeHtml(item.cart_number || '-') + '</td>'
|
||||
+ '</tr>'
|
||||
}).join('')
|
||||
+ '</tbody></table></div>'
|
||||
}
|
||||
|
||||
function renderOrderTable(items) {
|
||||
if (!items.length) {
|
||||
return '<div class="empty">当前用户暂无订单记录。</div>'
|
||||
}
|
||||
|
||||
return '<div class="table-wrap"><table><thead><tr>'
|
||||
+ '<th>订单编号</th><th>来源</th><th>来源ID</th><th>金额</th><th>状态</th><th>下单时间</th>'
|
||||
+ '</tr></thead><tbody>'
|
||||
+ items.map(function (item) {
|
||||
return '<tr>'
|
||||
+ '<td data-label="订单编号"><div>' + escapeHtml(item.order_number || item.order_id || '-') + '</div><div class="muted">' + escapeHtml(item.order_id || '-') + '</div></td>'
|
||||
+ '<td data-label="来源">' + escapeHtml(item.order_source || '-') + '</td>'
|
||||
+ '<td data-label="来源ID">' + escapeHtml(item.order_source_id || '-') + '</td>'
|
||||
+ '<td data-label="金额">¥' + escapeHtml(item.order_amount || 0) + '</td>'
|
||||
+ '<td data-label="状态">' + escapeHtml(item.order_status || '-') + '</td>'
|
||||
+ '<td data-label="下单时间">' + escapeHtml(item.order_create || item.created || '-') + '</td>'
|
||||
+ '</tr>'
|
||||
}).join('')
|
||||
+ '</tbody></table></div>'
|
||||
}
|
||||
|
||||
function renderDetail() {
|
||||
const user = getSelectedUser()
|
||||
if (!user) {
|
||||
detailWrapEl.innerHTML = '<div class="empty">请选择左侧用户查看购物车与订单详情。</div>'
|
||||
return
|
||||
}
|
||||
|
||||
detailWrapEl.innerHTML = '<div class="detail-header">'
|
||||
+ '<div>'
|
||||
+ '<h2 class="detail-title">' + escapeHtml(user.users_name || user.users_id || user.openid) + '</h2>'
|
||||
+ '<div class="detail-meta">openid:' + escapeHtml(user.openid || '-') + '</div>'
|
||||
+ '<div class="detail-meta">users_id:' + escapeHtml(user.users_id || '-') + '</div>'
|
||||
+ '<div class="detail-meta">手机号:' + escapeHtml(user.users_phone || '-') + '</div>'
|
||||
+ '</div>'
|
||||
+ '</div>'
|
||||
+ '<div class="summary-grid">'
|
||||
+ '<div class="summary-card"><div class="summary-label">购物车记录数</div><div class="summary-value">' + escapeHtml(user.cart_count || 0) + '</div></div>'
|
||||
+ '<div class="summary-card"><div class="summary-label">购物车商品总数</div><div class="summary-value">' + escapeHtml(user.cart_total_quantity || 0) + '</div></div>'
|
||||
+ '<div class="summary-card"><div class="summary-label">订单数</div><div class="summary-value">' + escapeHtml(user.order_count || 0) + '</div></div>'
|
||||
+ '<div class="summary-card"><div class="summary-label">订单总金额</div><div class="summary-value">¥' + escapeHtml(user.order_total_amount || 0) + '</div></div>'
|
||||
+ '</div>'
|
||||
+ '<div class="section"><h3 class="section-title">当前购物车详情</h3>' + renderCartTable(Array.isArray(user.carts) ? user.carts : []) + '</div>'
|
||||
+ '<div class="section"><h3 class="section-title">订单记录</h3>' + renderOrderTable(Array.isArray(user.orders) ? user.orders : []) + '</div>'
|
||||
}
|
||||
|
||||
async function loadUsers() {
|
||||
setStatus('正在加载用户购物车与订单数据...', '')
|
||||
try {
|
||||
const data = await requestJson('/cart-order/manage-users', {
|
||||
keyword: normalizeText(keywordInput.value),
|
||||
})
|
||||
state.users = Array.isArray(data.items) ? data.items : []
|
||||
if (!state.users.length) {
|
||||
state.selectedOpenid = ''
|
||||
} else if (!getSelectedUser()) {
|
||||
state.selectedOpenid = normalizeText(state.users[0].openid)
|
||||
}
|
||||
renderUserList()
|
||||
renderDetail()
|
||||
setStatus('加载完成,共 ' + state.users.length + ' 位用户。', 'success')
|
||||
} catch (err) {
|
||||
renderUserList()
|
||||
renderDetail()
|
||||
setStatus(err.message || '加载失败', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
userListEl.addEventListener('click', function (event) {
|
||||
const target = event.target && event.target.closest ? event.target.closest('[data-openid]') : null
|
||||
if (!target) {
|
||||
return
|
||||
}
|
||||
state.selectedOpenid = normalizeText(target.getAttribute('data-openid'))
|
||||
renderUserList()
|
||||
renderDetail()
|
||||
})
|
||||
|
||||
document.getElementById('searchBtn').addEventListener('click', loadUsers)
|
||||
document.getElementById('reloadBtn').addEventListener('click', loadUsers)
|
||||
document.getElementById('refreshBtn').addEventListener('click', loadUsers)
|
||||
document.getElementById('resetBtn').addEventListener('click', function () {
|
||||
keywordInput.value = ''
|
||||
loadUsers()
|
||||
})
|
||||
document.getElementById('logoutBtn').addEventListener('click', function () {
|
||||
localStorage.removeItem('pb_manage_token')
|
||||
localStorage.removeItem('pb_manage_logged_in')
|
||||
localStorage.removeItem('pb_manage_login_account')
|
||||
localStorage.removeItem('pb_manage_login_time')
|
||||
window.location.replace('/pb/manage/login')
|
||||
})
|
||||
|
||||
loadUsers()
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
const html = $template.loadFiles(
|
||||
__hooks + '/bai_web_pb_hooks/views/cart-order-manage.html',
|
||||
__hooks + '/bai_web_pb_hooks/shared/theme-head.html',
|
||||
__hooks + '/bai_web_pb_hooks/shared/theme-body.html'
|
||||
).render({})
|
||||
|
||||
return e.html(200, html)
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,98 +1,9 @@
|
||||
routerAdd('GET', '/manage', function (e) {
|
||||
const html = `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>管理主页</title>
|
||||
<script>
|
||||
(function () {
|
||||
var token = localStorage.getItem('pb_manage_token') || ''
|
||||
var isLoggedIn = localStorage.getItem('pb_manage_logged_in') === '1'
|
||||
if (!token || !isLoggedIn) {
|
||||
window.location.replace('/pb/manage/login')
|
||||
}
|
||||
})()
|
||||
</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: 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; }
|
||||
.module + .module { margin-top: 18px; }
|
||||
.module-title { margin: 0 0 10px; font-size: 22px; color: #0f172a; }
|
||||
.grid { display: grid; grid-template-columns: repeat(3, minmax(0, 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: 14px; display: flex; justify-content: flex-start; }
|
||||
@media (max-width: 960px) {
|
||||
.grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="wrap">
|
||||
<section class="hero">
|
||||
<h1>管理主页</h1>
|
||||
<section class="module">
|
||||
<h2 class="module-title">平台管理</h2>
|
||||
<div class="grid">
|
||||
<article class="card">
|
||||
<h2>字典管理</h2>
|
||||
<a class="btn" href="/pb/manage/dictionary-manage">进入字典管理</a>
|
||||
</article>
|
||||
<article class="card">
|
||||
<h2>文档管理</h2>
|
||||
<a class="btn" href="/pb/manage/document-manage">进入文档管理</a>
|
||||
</article>
|
||||
<article class="card">
|
||||
<h2>产品管理</h2>
|
||||
<a class="btn" href="/pb/manage/product-manage">进入产品管理</a>
|
||||
</article>
|
||||
<article class="card">
|
||||
<h2>SDK 权限管理</h2>
|
||||
<a class="btn" href="/pb/manage/sdk-permission-manage">进入权限管理</a>
|
||||
</article>
|
||||
<article class="card">
|
||||
<h2>购物车与订单</h2>
|
||||
<a class="btn" href="/pb/manage/cart-order-manage">进入购物车与订单管理</a>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
<section class="module">
|
||||
<h2 class="module-title">AI 管理</h2>
|
||||
<div class="grid">
|
||||
<article class="card">
|
||||
<h2>AI 审计管理</h2>
|
||||
<a class="btn" href="/pb/bai-ai-manage">进入审计管理</a>
|
||||
</article>
|
||||
<article class="card">
|
||||
<h2>AI 聊天测试</h2>
|
||||
<a class="btn" href="/pb/bai-chat">进入聊天测试</a>
|
||||
</article>
|
||||
<article class="card">
|
||||
<h2>SQL 实验室</h2>
|
||||
<a class="btn" href="/pb/bai-ai-sql-lab">进入 SQL 实验室</a>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
<div class="actions">
|
||||
<button id="logoutBtn" class="btn" type="button" style="background:#dc2626;">退出登录</button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<script>
|
||||
document.getElementById('logoutBtn').addEventListener('click', function () {
|
||||
localStorage.removeItem('pb_manage_token')
|
||||
localStorage.removeItem('pb_manage_logged_in')
|
||||
localStorage.removeItem('pb_manage_login_account')
|
||||
localStorage.removeItem('pb_manage_login_time')
|
||||
window.location.replace('/pb/manage/login')
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
const html = $template.loadFiles(
|
||||
__hooks + '/bai_web_pb_hooks/views/index.html',
|
||||
__hooks + '/bai_web_pb_hooks/shared/theme-head.html',
|
||||
__hooks + '/bai_web_pb_hooks/shared/theme-body.html'
|
||||
).render({})
|
||||
|
||||
return e.html(200, html)
|
||||
})
|
||||
|
||||
@@ -1,157 +1,9 @@
|
||||
function renderLoginPage(e) {
|
||||
const html = `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>登录</title>
|
||||
<script>
|
||||
(function () {
|
||||
var token = localStorage.getItem('pb_manage_token') || ''
|
||||
var isLoggedIn = localStorage.getItem('pb_manage_logged_in') === '1'
|
||||
if (token && isLoggedIn) {
|
||||
window.location.replace('/pb/manage')
|
||||
}
|
||||
})()
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
background: linear-gradient(180deg, #eff6ff 0%, #f8fafc 100%);
|
||||
color: #1f2937;
|
||||
}
|
||||
.wrap {
|
||||
max-width: 1440px;
|
||||
margin: 0 auto;
|
||||
padding: 34px 14px;
|
||||
}
|
||||
.card {
|
||||
max-width: 420px;
|
||||
margin: 0 auto;
|
||||
background: #fff;
|
||||
border: 1px solid #dbe3f0;
|
||||
border-radius: 18px;
|
||||
padding: 22px;
|
||||
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
h1 { margin-top: 0; }
|
||||
p { line-height: 1.8; color: #4b5563; margin-bottom: 20px; }
|
||||
.field { margin-bottom: 14px; }
|
||||
.field label { display: block; margin-bottom: 8px; color: #334155; font-weight: 600; }
|
||||
.field input {
|
||||
width: 100%;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 12px;
|
||||
padding: 10px 12px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.btn {
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
padding: 11px 14px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
.status { margin-top: 12px; min-height: 22px; font-size: 14px; }
|
||||
.status.error { color: #b91c1c; }
|
||||
.status.success { color: #15803d; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="wrap">
|
||||
<section class="card">
|
||||
<h1>后台登录</h1>
|
||||
<p>请输入平台账号(邮箱或手机号)与密码,登录成功后会自动跳转到管理首页。</p>
|
||||
<form id="loginForm">
|
||||
<div class="field">
|
||||
<label for="account">登录账号</label>
|
||||
<input id="account" name="account" placeholder="邮箱或手机号" required />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="password">密码</label>
|
||||
<input id="password" name="password" type="password" placeholder="请输入密码" required />
|
||||
</div>
|
||||
<button class="btn" id="submitBtn" type="submit">登录</button>
|
||||
</form>
|
||||
<div id="status" class="status"></div>
|
||||
</section>
|
||||
</main>
|
||||
<script>
|
||||
(function () {
|
||||
var API_BASE = '/pb/api'
|
||||
var form = document.getElementById('loginForm')
|
||||
var accountInput = document.getElementById('account')
|
||||
var passwordInput = document.getElementById('password')
|
||||
var statusEl = document.getElementById('status')
|
||||
var submitBtn = document.getElementById('submitBtn')
|
||||
|
||||
function setStatus(message, type) {
|
||||
statusEl.textContent = message || ''
|
||||
statusEl.className = 'status' + (type ? ' ' + type : '')
|
||||
}
|
||||
|
||||
form.addEventListener('submit', async function (event) {
|
||||
event.preventDefault()
|
||||
|
||||
var loginAccount = accountInput.value.trim()
|
||||
var password = passwordInput.value
|
||||
if (!loginAccount || !password) {
|
||||
setStatus('请填写账号和密码', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
submitBtn.disabled = true
|
||||
submitBtn.textContent = '登录中...'
|
||||
setStatus('正在登录,请稍候...', '')
|
||||
|
||||
try {
|
||||
var response = await fetch(API_BASE + '/platform/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
login_account: loginAccount,
|
||||
password: password,
|
||||
}),
|
||||
})
|
||||
|
||||
var result = await response.json()
|
||||
if (!response.ok || !result || result.code >= 400) {
|
||||
throw new Error((result && result.msg) || '登录失败')
|
||||
}
|
||||
|
||||
var token = result.token || (result.data && result.data.token) || ''
|
||||
if (!token) {
|
||||
throw new Error('登录成功但未获取到 token')
|
||||
}
|
||||
|
||||
localStorage.setItem('pb_manage_token', token)
|
||||
localStorage.setItem('pb_manage_logged_in', '1')
|
||||
localStorage.setItem('pb_manage_login_account', loginAccount)
|
||||
localStorage.setItem('pb_manage_login_time', new Date().toISOString())
|
||||
|
||||
setStatus('登录成功,正在跳转...', 'success')
|
||||
window.location.replace('/pb/manage')
|
||||
} catch (error) {
|
||||
localStorage.removeItem('pb_manage_token')
|
||||
localStorage.removeItem('pb_manage_logged_in')
|
||||
setStatus(error.message || '登录失败', 'error')
|
||||
} finally {
|
||||
submitBtn.disabled = false
|
||||
submitBtn.textContent = '登录'
|
||||
}
|
||||
})
|
||||
})()
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
const html = $template.loadFiles(
|
||||
__hooks + '/bai_web_pb_hooks/views/login.html',
|
||||
__hooks + '/bai_web_pb_hooks/shared/theme-head.html',
|
||||
__hooks + '/bai_web_pb_hooks/shared/theme-body.html'
|
||||
).render({})
|
||||
|
||||
return e.html(200, html)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,745 +1,9 @@
|
||||
routerAdd('GET', '/manage/sdk-permission-manage', function (e) {
|
||||
const html = `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>SDK 权限管理</title>
|
||||
<script>
|
||||
(function () {
|
||||
var token = localStorage.getItem('pb_manage_token') || ''
|
||||
var isLoggedIn = localStorage.getItem('pb_manage_logged_in') === '1'
|
||||
if (!token || !isLoggedIn) {
|
||||
window.location.replace('/pb/manage/login')
|
||||
}
|
||||
})()
|
||||
</script>
|
||||
<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: #0f172a; }
|
||||
.wrap { 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, h3 { margin-top: 0; }
|
||||
p { color: #475569; line-height: 1.7; }
|
||||
.actions, .toolbar, .row-actions { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; }
|
||||
.btn { border: none; border-radius: 12px; padding: 10px 16px; font-weight: 700; cursor: pointer; text-decoration: none; display: inline-flex; align-items: center; justify-content: center; }
|
||||
.btn-primary { background: #2563eb; color: #fff; }
|
||||
.btn-light { background: #f8fafc; color: #334155; border: 1px solid #dbe3f0; }
|
||||
.btn-danger { background: #dc2626; color: #fff; }
|
||||
.btn-warning { background: #f59e0b; color: #fff; }
|
||||
.btn-success { background: #16a34a; color: #fff; }
|
||||
.status { margin-top: 12px; min-height: 24px; font-size: 14px; }
|
||||
.status.success { color: #15803d; }
|
||||
.status.error { color: #b91c1c; }
|
||||
.note { padding: 14px 16px; border-radius: 16px; background: #eff6ff; color: #1d4ed8; font-size: 14px; line-height: 1.7; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
thead { background: #eff6ff; }
|
||||
th, td { padding: 14px 12px; border-bottom: 1px solid #e5e7eb; text-align: left; vertical-align: top; }
|
||||
th { font-size: 13px; color: #475569; }
|
||||
tr:hover td { background: #f8fafc; }
|
||||
input, textarea, select { width: 100%; border: 1px solid #cbd5e1; border-radius: 12px; padding: 9px 11px; font-size: 14px; background: #fff; }
|
||||
textarea { min-height: 80px; resize: vertical; }
|
||||
.empty { text-align: center; padding: 24px 16px; color: #64748b; }
|
||||
.muted { color: #64748b; font-size: 12px; }
|
||||
.grid { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 12px; }
|
||||
.full { grid-column: 1 / -1; }
|
||||
.rule-grid { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 10px; }
|
||||
.rule-card { border: 1px solid #dbe3f0; border-radius: 16px; padding: 12px; background: #f8fbff; }
|
||||
.rule-card h4 { margin: 0 0 10px; font-size: 14px; display: flex; align-items: center; gap: 8px; }
|
||||
.rule-meta { display: flex; align-items: center; gap: 8px; margin-top: 8px; color: #475569; font-size: 12px; }
|
||||
.rule-meta input[type="checkbox"] { width: auto; margin: 0; }
|
||||
.rule-toggle { display: inline-flex; align-items: center; gap: 6px; color: #0f172a; font-size: 13px; font-weight: 600; }
|
||||
.rule-toggle input[type="checkbox"] { width: auto; margin: 0; }
|
||||
.split { display: grid; grid-template-columns: 1.15fr 1fr; gap: 14px; }
|
||||
.table-wrap { overflow: auto; }
|
||||
.collection-table { table-layout: fixed; }
|
||||
.collection-col { width: 264px; }
|
||||
.rule-col { width: calc(100% - 264px); }
|
||||
.collection-meta { display: flex; flex-direction: column; gap: 6px; align-items: flex-start; }
|
||||
.loading-mask { position: fixed; inset: 0; display: none; align-items: center; justify-content: center; padding: 24px; background: rgba(15, 23, 42, 0.42); backdrop-filter: blur(4px); z-index: 9998; }
|
||||
.loading-mask.show { display: flex; }
|
||||
.loading-card { min-width: min(92vw, 360px); padding: 24px 22px; border-radius: 20px; background: rgba(255,255,255,0.98); box-shadow: 0 28px 70px rgba(15, 23, 42, 0.22); border: 1px solid #dbe3f0; text-align: center; }
|
||||
.loading-spinner { width: 44px; height: 44px; margin: 0 auto 14px; border-radius: 999px; border: 4px solid #dbeafe; border-top-color: #2563eb; animation: sdkSpin 0.9s linear infinite; }
|
||||
.loading-text { color: #0f172a; font-size: 15px; font-weight: 700; }
|
||||
@keyframes sdkSpin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
||||
@media (max-width: 1100px) {
|
||||
.split, .rule-grid, .grid { grid-template-columns: 1fr; }
|
||||
.collection-col, .rule-col { width: auto; }
|
||||
table, thead, tbody, th, td, tr { display: block; }
|
||||
thead { display: none; }
|
||||
tr { margin-bottom: 14px; border: 1px solid #e5e7eb; border-radius: 16px; overflow: hidden; }
|
||||
td { display: flex; flex-direction: column; gap: 8px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="wrap">
|
||||
<section class="panel">
|
||||
<h1>SDK 权限管理</h1>
|
||||
<p>这里管理的是 <code>tbl_auth_users</code> 用户通过 PocketBase SDK 直连数据库时的业务权限。<strong>ManagePlatform</strong> 会被视为你的业务管理员,但不会自动变成 PocketBase 原生 <code>_superusers</code>。</p>
|
||||
<div class="actions">
|
||||
<a class="btn btn-light" href="/pb/manage">返回主页</a>
|
||||
<button class="btn btn-light" id="refreshBtn" type="button">刷新数据</button>
|
||||
<button class="btn btn-success" id="syncManageBtn" type="button">同步 ManagePlatform 全权限</button>
|
||||
<button class="btn btn-danger" id="logoutBtn" type="button">退出登录</button>
|
||||
</div>
|
||||
<div class="status" id="status"></div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="note" id="noteBox">加载中...</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>角色管理</h2>
|
||||
<div class="grid">
|
||||
<input id="newRoleName" placeholder="角色名称" />
|
||||
<input id="newRoleCode" placeholder="角色编码,可为空" />
|
||||
<input id="newRoleStatus" type="number" placeholder="状态,默认1" value="1" />
|
||||
<button class="btn btn-primary" id="createRoleBtn" type="button">新增角色</button>
|
||||
<div class="muted">角色 ID 由系统自动生成,页面不显示。</div>
|
||||
<textarea id="newRoleRemark" class="full" placeholder="备注"></textarea>
|
||||
</div>
|
||||
<div class="table-wrap" style="margin-top:16px;">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>名称</th>
|
||||
<th>编码</th>
|
||||
<th>状态</th>
|
||||
<th>备注</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="roleTableBody">
|
||||
<tr><td colspan="5" class="empty">暂无角色。</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>用户授权</h2>
|
||||
<div class="toolbar">
|
||||
<input id="userKeywordInput" placeholder="按姓名、手机号、openid、角色搜索" />
|
||||
<button class="btn btn-light" id="searchUserBtn" type="button">查询用户</button>
|
||||
</div>
|
||||
<div class="table-wrap" style="margin-top:16px;">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>用户</th>
|
||||
<th>身份类型</th>
|
||||
<th>当前角色</th>
|
||||
<th>授权</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="userTableBody">
|
||||
<tr><td colspan="5" class="empty">暂无用户。</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>Collection 直连权限</h2>
|
||||
<div class="toolbar">
|
||||
<select id="permissionRoleSelect"></select>
|
||||
<div class="muted">这里是按“当前配置角色”逐表分配 CRUD 权限。下面依次对应“列表、详情、新增、修改、删除”五种权限,勾选后会立即保存。公开访问或自定义规则的操作不会显示授权勾选框。</div>
|
||||
</div>
|
||||
<div class="table-wrap" style="margin-top:16px;">
|
||||
<table class="collection-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="collection-col">集合</th>
|
||||
<th class="rule-col">当前角色权限</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="collectionTableBody">
|
||||
<tr><td colspan="2" class="empty">暂无集合。</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<div class="loading-mask" id="loadingMask">
|
||||
<div class="loading-card">
|
||||
<div class="loading-spinner"></div>
|
||||
<div class="loading-text" id="loadingText">处理中,请稍候...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API_BASE = '/pb/api/sdk-permission'
|
||||
const tokenKey = 'pb_manage_token'
|
||||
const statusEl = document.getElementById('status')
|
||||
const noteBox = document.getElementById('noteBox')
|
||||
const roleTableBody = document.getElementById('roleTableBody')
|
||||
const userTableBody = document.getElementById('userTableBody')
|
||||
const collectionTableBody = document.getElementById('collectionTableBody')
|
||||
const permissionRoleSelect = document.getElementById('permissionRoleSelect')
|
||||
const loadingMask = document.getElementById('loadingMask')
|
||||
const loadingText = document.getElementById('loadingText')
|
||||
const loadingState = { count: 0 }
|
||||
const state = {
|
||||
roles: [],
|
||||
users: [],
|
||||
collections: [],
|
||||
userKeyword: '',
|
||||
selectedPermissionRoleId: '',
|
||||
}
|
||||
|
||||
function setStatus(message, type) {
|
||||
statusEl.textContent = message || ''
|
||||
statusEl.className = 'status' + (type ? ' ' + type : '')
|
||||
}
|
||||
|
||||
function showLoading(message) {
|
||||
loadingState.count += 1
|
||||
loadingText.textContent = message || '处理中,请稍候...'
|
||||
loadingMask.classList.add('show')
|
||||
}
|
||||
|
||||
function hideLoading() {
|
||||
loadingState.count = Math.max(0, loadingState.count - 1)
|
||||
if (!loadingState.count) {
|
||||
loadingMask.classList.remove('show')
|
||||
loadingText.textContent = '处理中,请稍候...'
|
||||
}
|
||||
}
|
||||
|
||||
function getToken() {
|
||||
return localStorage.getItem(tokenKey) || ''
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value || '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
function getRoleById(roleId) {
|
||||
const target = String(roleId || '')
|
||||
return state.roles.find(function (role) {
|
||||
return role.role_id === target
|
||||
}) || null
|
||||
}
|
||||
|
||||
function getRoleName(roleId) {
|
||||
const role = getRoleById(roleId)
|
||||
return role ? role.role_name : ''
|
||||
}
|
||||
|
||||
function syncSelectedPermissionRole() {
|
||||
const exists = state.roles.some(function (role) {
|
||||
return role.role_id === state.selectedPermissionRoleId
|
||||
})
|
||||
if (!exists) {
|
||||
state.selectedPermissionRoleId = state.roles.length ? state.roles[0].role_id : ''
|
||||
}
|
||||
}
|
||||
|
||||
function decodeErrMsg(value) {
|
||||
if (!value) {
|
||||
return ''
|
||||
}
|
||||
|
||||
try {
|
||||
return decodeURIComponent(value)
|
||||
} catch (_err) {
|
||||
return String(value)
|
||||
}
|
||||
}
|
||||
|
||||
function unwrapApiResponse(data, res) {
|
||||
const safe = data && typeof data === 'object' ? data : {}
|
||||
const headerCode = Number(res && res.headers ? (res.headers.get('X-Status-Code') || 0) : 0)
|
||||
const headerErrMsg = decodeErrMsg(res && res.headers ? res.headers.get('X-Err-Msg') : '')
|
||||
const code = Number(safe.statusCode || safe.code || headerCode || 0)
|
||||
const errMsg = safe.errMsg || safe.msg || headerErrMsg || ''
|
||||
const hasLegacyEnvelope = Object.prototype.hasOwnProperty.call(safe, 'data')
|
||||
&& (Object.prototype.hasOwnProperty.call(safe, 'code')
|
||||
|| Object.prototype.hasOwnProperty.call(safe, 'statusCode')
|
||||
|| Object.prototype.hasOwnProperty.call(safe, 'msg')
|
||||
|| Object.prototype.hasOwnProperty.call(safe, 'errMsg'))
|
||||
const payload = hasLegacyEnvelope ? (safe.data || {}) : safe
|
||||
return { code: code, errMsg: errMsg, payload: payload }
|
||||
}
|
||||
|
||||
async function requestJson(path, payload) {
|
||||
const token = getToken()
|
||||
if (!token) {
|
||||
localStorage.removeItem('pb_manage_logged_in')
|
||||
window.location.replace('/pb/manage/login')
|
||||
throw new Error('登录状态已失效,请重新登录')
|
||||
}
|
||||
|
||||
const res = await fetch(API_BASE + path, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: 'Bearer ' + token,
|
||||
},
|
||||
body: JSON.stringify(payload || {}),
|
||||
})
|
||||
|
||||
const data = await res.json()
|
||||
const unwrapped = unwrapApiResponse(data, res)
|
||||
if (!res.ok || !data || unwrapped.code >= 400) {
|
||||
if (res.status === 401 || res.status === 403 || unwrapped.code === 401 || unwrapped.code === 403) {
|
||||
localStorage.removeItem('pb_manage_token')
|
||||
localStorage.removeItem('pb_manage_logged_in')
|
||||
localStorage.removeItem('pb_manage_login_account')
|
||||
localStorage.removeItem('pb_manage_login_time')
|
||||
window.location.replace('/pb/manage/login')
|
||||
}
|
||||
throw new Error(unwrapped.errMsg || '请求失败')
|
||||
}
|
||||
|
||||
return unwrapped.payload
|
||||
}
|
||||
|
||||
function roleOptionsHtml(selectedRoleId) {
|
||||
const current = String(selectedRoleId || '')
|
||||
return ['<option value="">未分配</option>'].concat(state.roles.map(function (role) {
|
||||
return '<option value="' + escapeHtml(role.role_id) + '"' + (current === role.role_id ? ' selected' : '') + '>' + escapeHtml(role.role_name) + '</option>'
|
||||
})).join('')
|
||||
}
|
||||
|
||||
function renderRoles() {
|
||||
if (!state.roles.length) {
|
||||
roleTableBody.innerHTML = '<tr><td colspan="5" class="empty">暂无角色。</td></tr>'
|
||||
return
|
||||
}
|
||||
|
||||
roleTableBody.innerHTML = state.roles.map(function (role) {
|
||||
return '<tr data-role-id="' + escapeHtml(role.role_id) + '">'
|
||||
+ '<td><input data-role-field="role_name" value="' + escapeHtml(role.role_name) + '" /></td>'
|
||||
+ '<td><input data-role-field="role_code" value="' + escapeHtml(role.role_code) + '" /></td>'
|
||||
+ '<td><input data-role-field="role_status" type="number" value="' + escapeHtml(role.role_status) + '" /></td>'
|
||||
+ '<td><textarea data-role-field="role_remark">' + escapeHtml(role.role_remark) + '</textarea></td>'
|
||||
+ '<td><div class="row-actions"><button class="btn btn-light" type="button" onclick="window.__saveRoleRow(\\'' + encodeURIComponent(role.role_id) + '\\')">保存</button><button class="btn btn-danger" type="button" onclick="window.__deleteRoleRow(\\'' + encodeURIComponent(role.role_id) + '\\')">删除</button></div></td>'
|
||||
+ '</tr>'
|
||||
}).join('')
|
||||
}
|
||||
|
||||
function renderUsers() {
|
||||
if (!state.users.length) {
|
||||
userTableBody.innerHTML = '<tr><td colspan="5" class="empty">暂无匹配用户。</td></tr>'
|
||||
return
|
||||
}
|
||||
|
||||
userTableBody.innerHTML = state.users.map(function (user) {
|
||||
const name = user.users_name || '未命名用户'
|
||||
const phone = user.users_phone || '无手机号'
|
||||
const roleName = user.role_name || getRoleName(user.usergroups_id) || '未分配'
|
||||
return '<tr data-user-id="' + escapeHtml(user.pb_id) + '">'
|
||||
+ '<td><div><strong>' + escapeHtml(name) + '</strong></div><div class="muted">' + escapeHtml(phone) + '</div><div class="muted">' + escapeHtml(user.openid) + '</div></td>'
|
||||
+ '<td><div>' + escapeHtml(user.users_idtype || '') + '</div><div class="muted">' + escapeHtml(user.users_type || '') + '</div></td>'
|
||||
+ '<td>' + escapeHtml(roleName) + '</td>'
|
||||
+ '<td><select data-user-role-select="1">' + roleOptionsHtml(user.usergroups_id) + '</select></td>'
|
||||
+ '<td><button class="btn btn-light" type="button" onclick="window.__saveUserRole(\\'' + escapeHtml(user.pb_id) + '\\')">保存角色</button></td>'
|
||||
+ '</tr>'
|
||||
}).join('')
|
||||
}
|
||||
|
||||
function renderPermissionRoleOptions() {
|
||||
if (!state.roles.length) {
|
||||
permissionRoleSelect.innerHTML = '<option value="">暂无角色</option>'
|
||||
permissionRoleSelect.disabled = true
|
||||
return
|
||||
}
|
||||
|
||||
permissionRoleSelect.disabled = false
|
||||
permissionRoleSelect.innerHTML = state.roles.map(function (role) {
|
||||
return '<option value="' + escapeHtml(role.role_id) + '"' + (role.role_id === state.selectedPermissionRoleId ? ' selected' : '') + '>' + escapeHtml(role.role_name) + '</option>'
|
||||
}).join('')
|
||||
}
|
||||
|
||||
function getOperationLabel(operation) {
|
||||
const map = {
|
||||
list: '列表',
|
||||
view: '详情',
|
||||
create: '新增',
|
||||
update: '修改',
|
||||
delete: '删除',
|
||||
}
|
||||
return map[operation] || operation
|
||||
}
|
||||
|
||||
function getRuleSummary(config, selectedRoleId) {
|
||||
const current = config || { mode: 'locked', includeManagePlatform: false, roles: [], rawExpression: '' }
|
||||
const items = []
|
||||
if (current.mode === 'public') items.push('公开可访问')
|
||||
if (current.mode === 'authenticated') items.push('登录用户可访问')
|
||||
if (current.includeManagePlatform) items.push('含 ManagePlatform')
|
||||
if (current.mode === 'custom') items.push('自定义规则')
|
||||
if (Array.isArray(current.roles) && current.roles.length) {
|
||||
items.push('已分配角色数:' + current.roles.length)
|
||||
}
|
||||
return items.length ? items.join(',') : '当前无额外说明'
|
||||
}
|
||||
|
||||
function canControlRule(config) {
|
||||
const current = config || { mode: 'locked', includeManagePlatform: false, roles: [], rawExpression: '' }
|
||||
return !!state.selectedPermissionRoleId && current.mode !== 'custom' && current.mode !== 'public'
|
||||
}
|
||||
|
||||
function isCollectionFullyChecked(collection) {
|
||||
if (!collection || !collection.parsedRules) {
|
||||
return false
|
||||
}
|
||||
|
||||
const operations = ['list', 'view', 'create', 'update', 'delete']
|
||||
let controllableCount = 0
|
||||
let checkedCount = 0
|
||||
|
||||
for (let i = 0; i < operations.length; i += 1) {
|
||||
const config = collection.parsedRules[operations[i]]
|
||||
if (!canControlRule(config)) {
|
||||
continue
|
||||
}
|
||||
controllableCount += 1
|
||||
if (config && Array.isArray(config.roles) && config.roles.indexOf(state.selectedPermissionRoleId) !== -1) {
|
||||
checkedCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
return controllableCount > 0 && controllableCount === checkedCount
|
||||
}
|
||||
|
||||
function getCollectionControllableCount(collection) {
|
||||
if (!collection || !collection.parsedRules) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const operations = ['list', 'view', 'create', 'update', 'delete']
|
||||
let controllableCount = 0
|
||||
|
||||
for (let i = 0; i < operations.length; i += 1) {
|
||||
if (canControlRule(collection.parsedRules[operations[i]])) {
|
||||
controllableCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
return controllableCount
|
||||
}
|
||||
|
||||
function renderRuleCard(collectionName, operation, config) {
|
||||
const current = config || { mode: 'locked', includeManagePlatform: false, roles: [], rawExpression: '' }
|
||||
const selectedRoleId = state.selectedPermissionRoleId
|
||||
const checked = selectedRoleId && Array.isArray(current.roles) && current.roles.indexOf(selectedRoleId) !== -1
|
||||
const canControl = canControlRule(current)
|
||||
const summary = current.mode === 'public'
|
||||
? '可公开访问'
|
||||
: getRuleSummary(current, selectedRoleId)
|
||||
return '<div class="rule-card" data-collection="' + escapeHtml(collectionName) + '" data-op="' + operation + '">'
|
||||
+ '<h4>' + (canControl ? '<label class="rule-toggle"><input type="checkbox" data-rule-field="allowSelectedRole"' + (checked ? ' checked' : '') + ' /></label>' : '') + '<span>' + getOperationLabel(operation) + '</span></h4>'
|
||||
+ '<div class="muted" style="margin-top:8px;">' + escapeHtml(summary) + '</div>'
|
||||
+ (current.mode === 'custom' ? '<div class="muted" style="margin-top:8px;">当前操作使用 custom 规则,禁止修改</div>' : '')
|
||||
+ '</div>'
|
||||
}
|
||||
|
||||
function renderCollections() {
|
||||
if (!state.collections.length) {
|
||||
collectionTableBody.innerHTML = '<tr><td colspan="2" class="empty">暂无可管理集合。</td></tr>'
|
||||
return
|
||||
}
|
||||
|
||||
collectionTableBody.innerHTML = state.collections.map(function (collection) {
|
||||
const allChecked = isCollectionFullyChecked(collection)
|
||||
const controllableCount = getCollectionControllableCount(collection)
|
||||
return '<tr data-collection-row="' + escapeHtml(collection.name) + '">'
|
||||
+ '<td class="collection-col"><div class="collection-meta"><div><strong>' + escapeHtml(collection.name) + '</strong></div><div class="muted">' + escapeHtml(collection.type) + '</div><label class="rule-toggle"><input type="checkbox" data-rule-field="toggleCollection"' + (allChecked ? ' checked' : '') + (state.selectedPermissionRoleId && controllableCount > 0 ? '' : ' disabled') + ' /><span>全选</span></label></div></td>'
|
||||
+ '<td><div class="rule-grid">'
|
||||
+ renderRuleCard(collection.name, 'list', collection.parsedRules.list)
|
||||
+ renderRuleCard(collection.name, 'view', collection.parsedRules.view)
|
||||
+ renderRuleCard(collection.name, 'create', collection.parsedRules.create)
|
||||
+ renderRuleCard(collection.name, 'update', collection.parsedRules.update)
|
||||
+ renderRuleCard(collection.name, 'delete', collection.parsedRules.delete)
|
||||
+ '</div></td>'
|
||||
+ '</tr>'
|
||||
}).join('')
|
||||
}
|
||||
|
||||
async function loadContext() {
|
||||
showLoading('正在加载权限管理数据...')
|
||||
setStatus('正在加载权限管理数据...', '')
|
||||
try {
|
||||
const data = await requestJson('/context', { keyword: state.userKeyword })
|
||||
state.roles = Array.isArray(data.roles) ? data.roles : []
|
||||
state.users = Array.isArray(data.users) ? data.users : []
|
||||
state.collections = Array.isArray(data.collections) ? data.collections : []
|
||||
syncSelectedPermissionRole()
|
||||
noteBox.textContent = data.note || '权限管理说明已加载。'
|
||||
renderRoles()
|
||||
renderUsers()
|
||||
renderPermissionRoleOptions()
|
||||
renderCollections()
|
||||
setStatus('权限管理数据已刷新。', 'success')
|
||||
} catch (err) {
|
||||
setStatus(err.message || '加载权限管理数据失败', 'error')
|
||||
} finally {
|
||||
hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
function getRoleRowPayload(roleId) {
|
||||
const targetId = decodeURIComponent(roleId)
|
||||
const row = roleTableBody.querySelector('[data-role-id="' + targetId.replace(/"/g, '\\"') + '"]')
|
||||
if (!row) {
|
||||
throw new Error('未找到对应角色行')
|
||||
}
|
||||
|
||||
const find = function (fieldName) {
|
||||
const input = row.querySelector('[data-role-field="' + fieldName + '"]')
|
||||
return input ? input.value : ''
|
||||
}
|
||||
|
||||
return {
|
||||
original_role_id: targetId,
|
||||
role_name: find('role_name'),
|
||||
role_code: find('role_code'),
|
||||
role_status: find('role_status'),
|
||||
role_remark: find('role_remark'),
|
||||
}
|
||||
}
|
||||
|
||||
function getRuleBoxConfig(collectionName, operation) {
|
||||
const collection = state.collections.find(function (item) {
|
||||
return item.name === collectionName
|
||||
})
|
||||
const current = collection && collection.parsedRules ? collection.parsedRules[operation] : null
|
||||
const base = current || {
|
||||
mode: 'locked',
|
||||
includeManagePlatform: false,
|
||||
roles: [],
|
||||
rawExpression: '',
|
||||
}
|
||||
if (base.mode === 'custom') {
|
||||
return {
|
||||
mode: 'custom',
|
||||
includeManagePlatform: !!base.includeManagePlatform,
|
||||
roles: Array.isArray(base.roles) ? base.roles.slice() : [],
|
||||
rawExpression: base.rawExpression || '',
|
||||
}
|
||||
}
|
||||
|
||||
const box = collectionTableBody.querySelector('.rule-card[data-collection="' + collectionName.replace(/"/g, '\\"') + '"][data-op="' + operation + '"]')
|
||||
const allowEl = box ? box.querySelector('[data-rule-field="allowSelectedRole"]') : null
|
||||
const roles = Array.isArray(base.roles) ? base.roles.slice() : []
|
||||
const selectedRoleId = state.selectedPermissionRoleId
|
||||
const nextRoles = roles.filter(function (roleId) {
|
||||
return roleId !== selectedRoleId
|
||||
})
|
||||
|
||||
if (selectedRoleId && allowEl && allowEl.checked && nextRoles.indexOf(selectedRoleId) === -1) {
|
||||
nextRoles.push(selectedRoleId)
|
||||
}
|
||||
|
||||
let nextMode = base.mode
|
||||
if (nextMode === 'locked' && nextRoles.length) {
|
||||
nextMode = 'roleBased'
|
||||
} else if (nextMode === 'roleBased' && !nextRoles.length && !base.includeManagePlatform) {
|
||||
nextMode = 'locked'
|
||||
}
|
||||
|
||||
return {
|
||||
mode: nextMode,
|
||||
includeManagePlatform: !!base.includeManagePlatform,
|
||||
roles: nextRoles,
|
||||
rawExpression: base.rawExpression || '',
|
||||
}
|
||||
}
|
||||
|
||||
async function createRole() {
|
||||
const payload = {
|
||||
role_name: document.getElementById('newRoleName').value.trim(),
|
||||
role_code: document.getElementById('newRoleCode').value.trim(),
|
||||
role_status: document.getElementById('newRoleStatus').value.trim() || '1',
|
||||
role_remark: document.getElementById('newRoleRemark').value.trim(),
|
||||
}
|
||||
|
||||
showLoading('正在新增角色...')
|
||||
try {
|
||||
await requestJson('/role-save', payload)
|
||||
document.getElementById('newRoleName').value = ''
|
||||
document.getElementById('newRoleCode').value = ''
|
||||
document.getElementById('newRoleStatus').value = '1'
|
||||
document.getElementById('newRoleRemark').value = ''
|
||||
await loadContext()
|
||||
setStatus('角色新增成功。', 'success')
|
||||
} catch (err) {
|
||||
setStatus(err.message || '新增角色失败', 'error')
|
||||
} finally {
|
||||
hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
async function saveRoleRow(roleId) {
|
||||
showLoading('正在保存角色...')
|
||||
try {
|
||||
await requestJson('/role-save', getRoleRowPayload(roleId))
|
||||
await loadContext()
|
||||
setStatus('角色保存成功。', 'success')
|
||||
} catch (err) {
|
||||
setStatus(err.message || '保存角色失败', 'error')
|
||||
} finally {
|
||||
hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteRoleRow(roleId) {
|
||||
const targetId = decodeURIComponent(roleId)
|
||||
if (!window.confirm('确认删除角色「' + targetId + '」吗?这会清空绑定该角色的用户,并从已解析的集合规则中移除它。')) {
|
||||
return
|
||||
}
|
||||
|
||||
showLoading('正在删除角色...')
|
||||
try {
|
||||
await requestJson('/role-delete', { role_id: targetId })
|
||||
await loadContext()
|
||||
setStatus('角色删除成功。', 'success')
|
||||
} catch (err) {
|
||||
setStatus(err.message || '删除角色失败', 'error')
|
||||
} finally {
|
||||
hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
async function saveUserRole(pbId) {
|
||||
const row = userTableBody.querySelector('[data-user-id="' + pbId.replace(/"/g, '\\"') + '"]')
|
||||
const select = row ? row.querySelector('[data-user-role-select="1"]') : null
|
||||
const payload = {
|
||||
pb_id: pbId,
|
||||
usergroups_id: select ? select.value : '',
|
||||
}
|
||||
|
||||
showLoading('正在保存用户角色...')
|
||||
try {
|
||||
await requestJson('/user-role-update', payload)
|
||||
await loadContext()
|
||||
setStatus('用户角色已更新。', 'success')
|
||||
} catch (err) {
|
||||
setStatus(err.message || '更新用户角色失败', 'error')
|
||||
} finally {
|
||||
hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
async function saveCollectionRules(collectionName) {
|
||||
const targetName = decodeURIComponent(collectionName)
|
||||
if (!state.selectedPermissionRoleId) {
|
||||
setStatus('请先选择一个要配置权限的角色。', 'error')
|
||||
return
|
||||
}
|
||||
const payload = {
|
||||
collection_name: targetName,
|
||||
rules: {
|
||||
list: getRuleBoxConfig(targetName, 'list'),
|
||||
view: getRuleBoxConfig(targetName, 'view'),
|
||||
create: getRuleBoxConfig(targetName, 'create'),
|
||||
update: getRuleBoxConfig(targetName, 'update'),
|
||||
delete: getRuleBoxConfig(targetName, 'delete'),
|
||||
},
|
||||
}
|
||||
|
||||
showLoading('正在同步集合权限...')
|
||||
try {
|
||||
await requestJson('/collection-save', payload)
|
||||
await loadContext()
|
||||
setStatus('已保存角色「' + (getRoleName(state.selectedPermissionRoleId) || '未命名角色') + '」在集合「' + targetName + '」上的权限。', 'success')
|
||||
} catch (err) {
|
||||
setStatus(err.message || '保存集合权限失败', 'error')
|
||||
} finally {
|
||||
hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
async function syncManagePlatform() {
|
||||
if (!window.confirm('确认将 ManagePlatform 同步为所有业务集合的全权限吗?这不会创建 _superusers,但会为业务表开放全部 CRUD。')) {
|
||||
return
|
||||
}
|
||||
|
||||
showLoading('正在同步 ManagePlatform 全权限...')
|
||||
try {
|
||||
const data = await requestJson('/manageplatform-sync', {})
|
||||
await loadContext()
|
||||
setStatus('已同步 ManagePlatform 全权限,共处理 ' + String((data && data.count) || 0) + ' 个集合。', 'success')
|
||||
} catch (err) {
|
||||
setStatus(err.message || '同步 ManagePlatform 全权限失败', 'error')
|
||||
} finally {
|
||||
hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
window.__saveRoleRow = saveRoleRow
|
||||
window.__deleteRoleRow = deleteRoleRow
|
||||
window.__saveUserRole = saveUserRole
|
||||
window.__saveCollectionRules = saveCollectionRules
|
||||
|
||||
document.getElementById('createRoleBtn').addEventListener('click', createRole)
|
||||
document.getElementById('refreshBtn').addEventListener('click', loadContext)
|
||||
document.getElementById('syncManageBtn').addEventListener('click', syncManagePlatform)
|
||||
permissionRoleSelect.addEventListener('change', function () {
|
||||
state.selectedPermissionRoleId = permissionRoleSelect.value || ''
|
||||
renderPermissionRoleOptions()
|
||||
renderCollections()
|
||||
})
|
||||
collectionTableBody.addEventListener('change', function (event) {
|
||||
const target = event.target
|
||||
if (!target) {
|
||||
return
|
||||
}
|
||||
|
||||
const field = target.getAttribute('data-rule-field')
|
||||
if (field === 'allowSelectedRole') {
|
||||
const box = target.closest('.rule-card')
|
||||
if (!box) {
|
||||
return
|
||||
}
|
||||
const collectionName = box.getAttribute('data-collection') || ''
|
||||
saveCollectionRules(collectionName)
|
||||
return
|
||||
}
|
||||
|
||||
if (field === 'toggleCollection') {
|
||||
const row = target.closest('[data-collection-row]')
|
||||
if (!row) {
|
||||
return
|
||||
}
|
||||
const checkboxes = row.querySelectorAll('[data-rule-field="allowSelectedRole"]')
|
||||
for (let i = 0; i < checkboxes.length; i += 1) {
|
||||
if (!checkboxes[i].disabled) {
|
||||
checkboxes[i].checked = !!target.checked
|
||||
}
|
||||
}
|
||||
const collectionName = row.getAttribute('data-collection-row') || ''
|
||||
saveCollectionRules(collectionName)
|
||||
}
|
||||
})
|
||||
document.getElementById('searchUserBtn').addEventListener('click', function () {
|
||||
state.userKeyword = document.getElementById('userKeywordInput').value.trim()
|
||||
loadContext()
|
||||
})
|
||||
document.getElementById('logoutBtn').addEventListener('click', function () {
|
||||
localStorage.removeItem('pb_manage_token')
|
||||
localStorage.removeItem('pb_manage_logged_in')
|
||||
localStorage.removeItem('pb_manage_login_account')
|
||||
localStorage.removeItem('pb_manage_login_time')
|
||||
window.location.replace('/pb/manage/login')
|
||||
})
|
||||
|
||||
loadContext()
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
const html = $template.loadFiles(
|
||||
__hooks + '/bai_web_pb_hooks/views/sdk-permission-manage.html',
|
||||
__hooks + '/bai_web_pb_hooks/shared/theme-head.html',
|
||||
__hooks + '/bai_web_pb_hooks/shared/theme-body.html'
|
||||
).render({})
|
||||
|
||||
return e.html(200, html)
|
||||
})
|
||||
|
||||
32
pocket-base/bai_web_pb_hooks/shared/theme-body.html
Normal file
32
pocket-base/bai_web_pb_hooks/shared/theme-body.html
Normal file
@@ -0,0 +1,32 @@
|
||||
{{ define "theme_body" }}
|
||||
<button id="themeToggleBtn" class="theme-toggle" type="button" aria-label="切换深色或浅色模式">深色模式</button>
|
||||
<script>
|
||||
(function () {
|
||||
var THEME_KEY = 'pb_manage_theme'
|
||||
var root = document.documentElement
|
||||
var toggleBtn = document.getElementById('themeToggleBtn')
|
||||
if (!toggleBtn) {
|
||||
return
|
||||
}
|
||||
|
||||
function getTheme() {
|
||||
var theme = root.getAttribute('data-theme')
|
||||
return theme === 'dark' ? 'dark' : 'light'
|
||||
}
|
||||
|
||||
function applyTheme(theme) {
|
||||
root.setAttribute('data-theme', theme)
|
||||
root.style.colorScheme = theme
|
||||
localStorage.setItem(THEME_KEY, theme)
|
||||
toggleBtn.textContent = theme === 'dark' ? '浅色模式' : '深色模式'
|
||||
toggleBtn.setAttribute('aria-pressed', theme === 'dark' ? 'true' : 'false')
|
||||
}
|
||||
|
||||
toggleBtn.addEventListener('click', function () {
|
||||
applyTheme(getTheme() === 'dark' ? 'light' : 'dark')
|
||||
})
|
||||
|
||||
applyTheme(getTheme())
|
||||
})()
|
||||
</script>
|
||||
{{ end }}
|
||||
186
pocket-base/bai_web_pb_hooks/shared/theme-head.html
Normal file
186
pocket-base/bai_web_pb_hooks/shared/theme-head.html
Normal file
@@ -0,0 +1,186 @@
|
||||
{{ define "theme_head" }}
|
||||
<script>
|
||||
(function () {
|
||||
var THEME_KEY = 'pb_manage_theme'
|
||||
var storedTheme = localStorage.getItem(THEME_KEY)
|
||||
var fallbackTheme = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||
var theme = storedTheme === 'dark' || storedTheme === 'light' ? storedTheme : fallbackTheme
|
||||
document.documentElement.setAttribute('data-theme', theme)
|
||||
document.documentElement.style.colorScheme = theme
|
||||
})()
|
||||
</script>
|
||||
<style>
|
||||
html { color-scheme: light; }
|
||||
html[data-theme="dark"] { color-scheme: dark; }
|
||||
.theme-toggle {
|
||||
position: fixed;
|
||||
top: 18px;
|
||||
right: 18px;
|
||||
z-index: 10000;
|
||||
min-width: 112px;
|
||||
min-height: 42px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.32);
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
color: #0f172a;
|
||||
box-shadow: 0 16px 40px rgba(15, 23, 42, 0.14);
|
||||
backdrop-filter: blur(14px);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
.theme-toggle:hover { transform: translateY(-1px); }
|
||||
.theme-toggle:active { transform: translateY(0); }
|
||||
html[data-theme="dark"] body {
|
||||
background: #000 !important;
|
||||
color: #e5e7eb !important;
|
||||
}
|
||||
html[data-theme="dark"] .hero,
|
||||
html[data-theme="dark"] .card,
|
||||
html[data-theme="dark"] .panel,
|
||||
html[data-theme="dark"] .topbar,
|
||||
html[data-theme="dark"] .modal-card,
|
||||
html[data-theme="dark"] .loading-card,
|
||||
html[data-theme="dark"] .note,
|
||||
html[data-theme="dark"] .option-box,
|
||||
html[data-theme="dark"] .option-item,
|
||||
html[data-theme="dark"] .param-table-wrap,
|
||||
html[data-theme="dark"] .item-table,
|
||||
html[data-theme="dark"] .table-wrap,
|
||||
html[data-theme="dark"] .rule-card,
|
||||
html[data-theme="dark"] .summary-card,
|
||||
html[data-theme="dark"] .user-card,
|
||||
html[data-theme="dark"] .image-tile,
|
||||
html[data-theme="dark"] .image-tile-thumb,
|
||||
html[data-theme="dark"] .thumb,
|
||||
html[data-theme="dark"] .thumb-card {
|
||||
background: rgba(0, 0, 0, 0.88) !important;
|
||||
border-color: rgba(148, 163, 184, 0.22) !important;
|
||||
color: #e5e7eb !important;
|
||||
box-shadow: 0 18px 50px rgba(2, 6, 23, 0.35) !important;
|
||||
}
|
||||
html[data-theme="dark"] h1,
|
||||
html[data-theme="dark"] h2,
|
||||
html[data-theme="dark"] h3,
|
||||
html[data-theme="dark"] label,
|
||||
html[data-theme="dark"] th,
|
||||
html[data-theme="dark"] strong,
|
||||
html[data-theme="dark"] .module-title,
|
||||
html[data-theme="dark"] .modal-title,
|
||||
html[data-theme="dark"] .loading-text {
|
||||
color: #f8fafc !important;
|
||||
}
|
||||
html[data-theme="dark"] p,
|
||||
html[data-theme="dark"] .muted,
|
||||
html[data-theme="dark"] .hint,
|
||||
html[data-theme="dark"] .thumb-caption,
|
||||
html[data-theme="dark"] .thumb-meta,
|
||||
html[data-theme="dark"] .image-tile-order,
|
||||
html[data-theme="dark"] .item-field-label,
|
||||
html[data-theme="dark"] .drop-tip,
|
||||
html[data-theme="dark"] .empty,
|
||||
html[data-theme="dark"] td::before {
|
||||
color: #94a3b8 !important;
|
||||
}
|
||||
html[data-theme="dark"] input,
|
||||
html[data-theme="dark"] textarea,
|
||||
html[data-theme="dark"] select {
|
||||
background: rgba(0, 0, 0, 0.78) !important;
|
||||
border-color: rgba(148, 163, 184, 0.28) !important;
|
||||
color: #f8fafc !important;
|
||||
}
|
||||
html[data-theme="dark"] input::placeholder,
|
||||
html[data-theme="dark"] textarea::placeholder {
|
||||
color: #94a3b8 !important;
|
||||
}
|
||||
html[data-theme="dark"] table thead,
|
||||
html[data-theme="dark"] thead {
|
||||
background: rgba(30, 41, 59, 0.92) !important;
|
||||
}
|
||||
html[data-theme="dark"] th,
|
||||
html[data-theme="dark"] td,
|
||||
html[data-theme="dark"] tr,
|
||||
html[data-theme="dark"] .item-table,
|
||||
html[data-theme="dark"] .param-table-wrap {
|
||||
border-color: rgba(148, 163, 184, 0.16) !important;
|
||||
}
|
||||
html[data-theme="dark"] tr:hover td {
|
||||
background: rgba(30, 41, 59, 0.68) !important;
|
||||
}
|
||||
html[data-theme="dark"] .btn-light,
|
||||
html[data-theme="dark"] .btn-secondary,
|
||||
html[data-theme="dark"] .btn:not(.btn-primary):not(.btn-danger):not(.btn-warning) {
|
||||
background: rgba(30, 41, 59, 0.9) !important;
|
||||
border-color: rgba(148, 163, 184, 0.25) !important;
|
||||
color: #e2e8f0 !important;
|
||||
}
|
||||
html[data-theme="dark"] .btn-primary {
|
||||
background: #3b82f6 !important;
|
||||
color: #eff6ff !important;
|
||||
}
|
||||
html[data-theme="dark"] .btn-danger {
|
||||
background: #dc2626 !important;
|
||||
color: #fee2e2 !important;
|
||||
}
|
||||
html[data-theme="dark"] .btn-warning {
|
||||
background: #d97706 !important;
|
||||
color: #fffbeb !important;
|
||||
}
|
||||
html[data-theme="dark"] .badge-on {
|
||||
background: rgba(34, 197, 94, 0.18) !important;
|
||||
color: #86efac !important;
|
||||
}
|
||||
html[data-theme="dark"] .badge-off {
|
||||
background: rgba(239, 68, 68, 0.16) !important;
|
||||
color: #fca5a5 !important;
|
||||
}
|
||||
html[data-theme="dark"] .selection-tag {
|
||||
background: rgba(37, 99, 235, 0.18) !important;
|
||||
color: #bfdbfe !important;
|
||||
}
|
||||
html[data-theme="dark"] .stat-tag {
|
||||
background: rgba(37, 99, 235, 0.18) !important;
|
||||
color: #bfdbfe !important;
|
||||
}
|
||||
html[data-theme="dark"] .user-card.active {
|
||||
background: rgba(10, 10, 10, 0.96) !important;
|
||||
border-color: #3b82f6 !important;
|
||||
box-shadow: inset 0 0 0 1px rgba(59, 130, 246, 0.45) !important;
|
||||
}
|
||||
html[data-theme="dark"] .note {
|
||||
color: #bfdbfe !important;
|
||||
}
|
||||
html[data-theme="dark"] .rule-meta,
|
||||
html[data-theme="dark"] .rule-toggle,
|
||||
html[data-theme="dark"] .user-name,
|
||||
html[data-theme="dark"] .detail-title,
|
||||
html[data-theme="dark"] .section-title,
|
||||
html[data-theme="dark"] .summary-value {
|
||||
color: #f8fafc !important;
|
||||
}
|
||||
html[data-theme="dark"] .modal,
|
||||
html[data-theme="dark"] .modal-mask,
|
||||
html[data-theme="dark"] .loading-mask,
|
||||
html[data-theme="dark"] .image-viewer {
|
||||
background: rgba(2, 6, 23, 0.7) !important;
|
||||
}
|
||||
html[data-theme="dark"] .loading-spinner {
|
||||
border-color: rgba(59, 130, 246, 0.16) !important;
|
||||
border-top-color: #60a5fa !important;
|
||||
}
|
||||
html[data-theme="dark"] .theme-toggle {
|
||||
background: rgba(15, 23, 42, 0.86);
|
||||
color: #f8fafc;
|
||||
border-color: rgba(148, 163, 184, 0.26);
|
||||
box-shadow: 0 18px 40px rgba(2, 6, 23, 0.42);
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.theme-toggle {
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
min-width: 96px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{{ end }}
|
||||
407
pocket-base/bai_web_pb_hooks/views/cart-order-manage.html
Normal file
407
pocket-base/bai_web_pb_hooks/views/cart-order-manage.html
Normal file
@@ -0,0 +1,407 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>用户信息及订单管理</title>
|
||||
<script>
|
||||
(function () {
|
||||
var token = localStorage.getItem('pb_manage_token') || ''
|
||||
var isLoggedIn = localStorage.getItem('pb_manage_logged_in') === '1'
|
||||
if (!token || !isLoggedIn) {
|
||||
window.location.replace('/pb/manage/login')
|
||||
}
|
||||
})()
|
||||
</script>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; background: linear-gradient(180deg, #eef4ff 0%, #f8fbff 100%); color: #1f2937; }
|
||||
.container { max-width: 1520px; margin: 0 auto; padding: 24px 14px 40px; }
|
||||
.panel { background: rgba(255,255,255,0.97); 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; }
|
||||
.actions { display: flex; flex-wrap: wrap; gap: 10px; }
|
||||
.toolbar { display: grid; grid-template-columns: minmax(0, 1fr) auto auto auto; gap: 10px; }
|
||||
.btn { border: none; border-radius: 12px; padding: 10px 16px; font-weight: 600; cursor: pointer; text-decoration: none; display: inline-flex; align-items: center; justify-content: center; }
|
||||
.btn-primary { background: #2563eb; color: #fff; }
|
||||
.btn-light { background: #f8fafc; color: #334155; border: 1px solid #dbe3f0; }
|
||||
.btn-danger { background: #dc2626; color: #fff; }
|
||||
.status { margin-top: 12px; min-height: 24px; font-size: 14px; }
|
||||
.status.success { color: #15803d; }
|
||||
.status.error { color: #b91c1c; }
|
||||
input, select, textarea { width: 100%; border: 1px solid #cbd5e1; border-radius: 12px; padding: 10px 12px; font-size: 14px; background: #fff; }
|
||||
textarea { min-height: 90px; resize: vertical; }
|
||||
.layout { display: grid; grid-template-columns: minmax(340px, 420px) minmax(0, 1fr); gap: 14px; align-items: start; }
|
||||
.user-list { display: grid; gap: 10px; max-height: 72vh; overflow: auto; }
|
||||
.user-card { border: 1px solid #dbe3f0; border-radius: 16px; padding: 14px; background: #f8fbff; cursor: pointer; }
|
||||
.user-card.active { border-color: #2563eb; background: #eff6ff; box-shadow: inset 0 0 0 1px #bfdbfe; }
|
||||
.user-name { font-size: 18px; font-weight: 700; color: #0f172a; }
|
||||
.user-meta { margin-top: 6px; color: #64748b; font-size: 13px; word-break: break-all; }
|
||||
.user-stats { margin-top: 10px; display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.stat-tag { display: inline-flex; align-items: center; padding: 4px 10px; border-radius: 999px; background: #dbeafe; color: #1d4ed8; font-size: 12px; font-weight: 700; }
|
||||
.detail-header { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 12px; align-items: flex-start; margin-bottom: 14px; }
|
||||
.detail-title { margin: 0; font-size: 22px; }
|
||||
.detail-meta { color: #64748b; font-size: 13px; line-height: 1.7; }
|
||||
.summary-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; margin-bottom: 14px; }
|
||||
.summary-card { border: 1px solid #dbe3f0; border-radius: 16px; padding: 14px; background: #f8fbff; }
|
||||
.summary-label { color: #64748b; font-size: 13px; }
|
||||
.summary-value { margin-top: 8px; font-size: 22px; font-weight: 700; color: #0f172a; }
|
||||
.profile-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin-bottom: 14px; }
|
||||
.field-block { display: grid; gap: 6px; }
|
||||
.field-label { font-size: 13px; color: #64748b; }
|
||||
.detail-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 14px; }
|
||||
.section + .section { margin-top: 14px; }
|
||||
.section-title { margin: 0 0 10px; font-size: 18px; color: #0f172a; }
|
||||
.table-wrap { overflow: auto; border: 1px solid #dbe3f0; border-radius: 16px; background: #fff; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
thead { background: #eff6ff; }
|
||||
th, td { padding: 12px; border-bottom: 1px solid #e5e7eb; text-align: left; vertical-align: top; }
|
||||
.muted { color: #64748b; font-size: 12px; }
|
||||
.empty { text-align: center; padding: 24px; color: #64748b; }
|
||||
@media (max-width: 1080px) {
|
||||
.layout { grid-template-columns: 1fr; }
|
||||
.summary-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.profile-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.toolbar { grid-template-columns: 1fr; }
|
||||
.summary-grid { grid-template-columns: 1fr; }
|
||||
table, thead, tbody, th, td, tr { display: block; }
|
||||
thead { display: none; }
|
||||
tr { border-bottom: 1px solid #e5e7eb; }
|
||||
td { display: flex; justify-content: space-between; gap: 10px; }
|
||||
td::before { content: attr(data-label); font-weight: 700; color: #475569; }
|
||||
}
|
||||
</style>
|
||||
{{ template "theme_head" . }}
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<section class="panel">
|
||||
<h1 style="margin-top:0;">用户信息及订单管理</h1>
|
||||
<div class="actions">
|
||||
<a class="btn btn-light" href="/pb/manage">返回主页</a>
|
||||
<button class="btn btn-light" id="reloadBtn" type="button">刷新</button>
|
||||
</div>
|
||||
<div class="status" id="status"></div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="toolbar">
|
||||
<input id="keywordInput" placeholder="按用户名 / openid / 手机号 / users_id 搜索" />
|
||||
<button class="btn btn-primary" id="searchBtn" type="button">搜索</button>
|
||||
<button class="btn btn-light" id="resetBtn" type="button">重置</button>
|
||||
<button class="btn btn-light" id="refreshBtn" type="button">重新加载</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="layout">
|
||||
<section class="panel">
|
||||
<h2 style="margin-top:0;">用户列表</h2>
|
||||
<div id="userList" class="user-list"></div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div id="detailWrap"></div>
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API_BASE = '/pb/api'
|
||||
const tokenKey = 'pb_manage_token'
|
||||
const state = {
|
||||
users: [],
|
||||
selectedOpenid: '',
|
||||
userLevelOptions: [],
|
||||
}
|
||||
|
||||
const statusEl = document.getElementById('status')
|
||||
const keywordInput = document.getElementById('keywordInput')
|
||||
const userListEl = document.getElementById('userList')
|
||||
const detailWrapEl = document.getElementById('detailWrap')
|
||||
|
||||
function normalizeText(value) {
|
||||
return String(value || '').trim()
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value || '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
function setStatus(message, type) {
|
||||
statusEl.textContent = message || ''
|
||||
statusEl.className = 'status' + (type ? ' ' + type : '')
|
||||
}
|
||||
|
||||
function getToken() {
|
||||
return localStorage.getItem(tokenKey) || ''
|
||||
}
|
||||
|
||||
async function requestJson(path, payload) {
|
||||
const token = getToken()
|
||||
if (!token) {
|
||||
window.location.replace('/pb/manage/login')
|
||||
throw new Error('登录状态已失效,请重新登录')
|
||||
}
|
||||
|
||||
const res = await fetch(API_BASE + path, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: 'Bearer ' + token,
|
||||
},
|
||||
body: JSON.stringify(payload || {}),
|
||||
})
|
||||
|
||||
const data = await res.json()
|
||||
if (!res.ok) {
|
||||
throw new Error((data && (data.errMsg || data.message)) || '请求失败')
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
function getSelectedUser() {
|
||||
return state.users.find(function (item) {
|
||||
return normalizeText(item.openid) === normalizeText(state.selectedOpenid)
|
||||
}) || null
|
||||
}
|
||||
|
||||
function renderUserList() {
|
||||
if (!state.users.length) {
|
||||
userListEl.innerHTML = '<div class="empty">暂无匹配用户。</div>'
|
||||
return
|
||||
}
|
||||
|
||||
userListEl.innerHTML = state.users.map(function (item) {
|
||||
const isActive = normalizeText(item.openid) === normalizeText(state.selectedOpenid)
|
||||
return '<div class="user-card' + (isActive ? ' active' : '') + '" data-openid="' + escapeHtml(item.openid) + '">'
|
||||
+ '<div class="user-name">' + escapeHtml(item.users_name || item.users_id || item.openid) + '</div>'
|
||||
+ '<div class="user-meta">openid:' + escapeHtml(item.openid || '-') + '</div>'
|
||||
+ '<div class="user-meta">手机号:' + escapeHtml(item.users_phone || '-') + '</div>'
|
||||
+ '<div class="user-meta">users_id:' + escapeHtml(item.users_id || '-') + '</div>'
|
||||
+ '<div class="user-meta">会员等级:' + escapeHtml(item.users_level_name || item.users_level || '-') + '</div>'
|
||||
+ '<div class="user-stats">'
|
||||
+ '<span class="stat-tag">购物车 ' + escapeHtml(item.cart_count || 0) + '</span>'
|
||||
+ '<span class="stat-tag">购物数量 ' + escapeHtml(item.cart_total_quantity || 0) + '</span>'
|
||||
+ '<span class="stat-tag">订单 ' + escapeHtml(item.order_count || 0) + '</span>'
|
||||
+ '</div>'
|
||||
+ '</div>'
|
||||
}).join('')
|
||||
}
|
||||
|
||||
function renderCartTable(items) {
|
||||
if (!items.length) {
|
||||
return '<div class="empty">当前用户暂无购物车记录。</div>'
|
||||
}
|
||||
|
||||
return '<div class="table-wrap"><table><thead><tr>'
|
||||
+ '<th>商品名称</th><th>型号</th><th>数量</th><th>单价</th><th>状态</th><th>加入时间</th><th>购物车名</th>'
|
||||
+ '</tr></thead><tbody>'
|
||||
+ items.map(function (item) {
|
||||
return '<tr>'
|
||||
+ '<td data-label="商品名称"><div>' + escapeHtml(item.product_name || item.cart_product_id || '-') + '</div><div class="muted">' + escapeHtml(item.cart_product_id || '-') + '</div></td>'
|
||||
+ '<td data-label="型号">' + escapeHtml(item.product_modelnumber || '-') + '</td>'
|
||||
+ '<td data-label="数量">' + escapeHtml(item.cart_product_quantity || 0) + '</td>'
|
||||
+ '<td data-label="单价">¥' + escapeHtml(item.cart_at_price || 0) + '</td>'
|
||||
+ '<td data-label="状态">' + escapeHtml(item.cart_status || '-') + '</td>'
|
||||
+ '<td data-label="加入时间">' + escapeHtml(item.cart_create || item.created || '-') + '</td>'
|
||||
+ '<td data-label="购物车名">' + escapeHtml(item.cart_number || '-') + '</td>'
|
||||
+ '</tr>'
|
||||
}).join('')
|
||||
+ '</tbody></table></div>'
|
||||
}
|
||||
|
||||
function renderUserLevelOptions(selectedValue) {
|
||||
const current = normalizeText(selectedValue)
|
||||
let html = '<option value="">未设置</option>'
|
||||
|
||||
for (let i = 0; i < state.userLevelOptions.length; i += 1) {
|
||||
const item = state.userLevelOptions[i]
|
||||
const value = normalizeText(item.value)
|
||||
html += '<option value="' + escapeHtml(value) + '"' + (value === current ? ' selected' : '') + '>'
|
||||
+ escapeHtml(item.label || item.value || '')
|
||||
+ '</option>'
|
||||
}
|
||||
|
||||
return html
|
||||
}
|
||||
|
||||
function renderUserProfileForm(user) {
|
||||
return '<div class="section"><h3 class="section-title">用户信息维护</h3>'
|
||||
+ '<div class="profile-grid">'
|
||||
+ '<div class="field-block"><label class="field-label" for="userUsersName">用户名称</label><input id="userUsersName" value="' + escapeHtml(user.users_name || '') + '" /></div>'
|
||||
+ '<div class="field-block"><label class="field-label" for="userUsersPhone">手机号</label><input id="userUsersPhone" value="' + escapeHtml(user.users_phone || '') + '" /></div>'
|
||||
+ '<div class="field-block"><label class="field-label" for="userUsersLevel">会员等级</label><select id="userUsersLevel">' + renderUserLevelOptions(user.users_level) + '</select></div>'
|
||||
+ '<div class="field-block"><label class="field-label" for="userUsersType">用户类型</label><input id="userUsersType" value="' + escapeHtml(user.users_type || '') + '" /></div>'
|
||||
+ '<div class="field-block"><label class="field-label" for="userUsersStatus">用户状态</label><input id="userUsersStatus" value="' + escapeHtml(user.users_status || '') + '" /></div>'
|
||||
+ '<div class="field-block"><label class="field-label" for="userUsersRankLevel">用户星级</label><input id="userUsersRankLevel" value="' + escapeHtml(user.users_rank_level === null || typeof user.users_rank_level === 'undefined' ? '' : user.users_rank_level) + '" /></div>'
|
||||
+ '<div class="field-block"><label class="field-label" for="userUsersAuthType">账户类型</label><input id="userUsersAuthType" value="' + escapeHtml(user.users_auth_type === null || typeof user.users_auth_type === 'undefined' ? '' : user.users_auth_type) + '" /></div>'
|
||||
+ '<div class="field-block"><label class="field-label" for="userCompanyId">公司ID</label><input id="userCompanyId" value="' + escapeHtml(user.company_id || '') + '" /></div>'
|
||||
+ '<div class="field-block"><label class="field-label" for="userUsersTag">用户标签</label><input id="userUsersTag" value="' + escapeHtml(user.users_tag || '') + '" /></div>'
|
||||
+ '<div class="field-block"><label class="field-label" for="userUsersParentId">上级用户ID</label><input id="userUsersParentId" value="' + escapeHtml(user.users_parent_id || '') + '" /></div>'
|
||||
+ '<div class="field-block"><label class="field-label" for="userUsersPromoCode">推广码</label><input id="userUsersPromoCode" value="' + escapeHtml(user.users_promo_code || '') + '" /></div>'
|
||||
+ '<div class="field-block"><label class="field-label" for="userUsergroupsId">用户组ID</label><input id="userUsergroupsId" value="' + escapeHtml(user.usergroups_id || '') + '" /></div>'
|
||||
+ '<div class="field-block"><label class="field-label" for="userUsersIdNumber">证件号</label><input id="userUsersIdNumber" value="' + escapeHtml(user.users_id_number || '') + '" /></div>'
|
||||
+ '<div class="field-block"><label class="field-label" for="userUsersPicture">头像附件ID</label><input id="userUsersPicture" value="' + escapeHtml(user.users_picture || '') + '" /></div>'
|
||||
+ '<div class="field-block"><label class="field-label" for="userUsersIdPicA">证件正面附件ID</label><input id="userUsersIdPicA" value="' + escapeHtml(user.users_id_pic_a || '') + '" /></div>'
|
||||
+ '<div class="field-block"><label class="field-label" for="userUsersIdPicB">证件反面附件ID</label><input id="userUsersIdPicB" value="' + escapeHtml(user.users_id_pic_b || '') + '" /></div>'
|
||||
+ '<div class="field-block"><label class="field-label" for="userUsersTitlePicture">资质附件ID</label><input id="userUsersTitlePicture" value="' + escapeHtml(user.users_title_picture || '') + '" /></div>'
|
||||
+ '</div>'
|
||||
+ '<div class="detail-actions"><button class="btn btn-primary" id="saveUserBtn" type="button">保存用户信息</button></div>'
|
||||
+ '</div>'
|
||||
}
|
||||
|
||||
function renderOrderTable(items) {
|
||||
if (!items.length) {
|
||||
return '<div class="empty">当前用户暂无订单记录。</div>'
|
||||
}
|
||||
|
||||
return '<div class="table-wrap"><table><thead><tr>'
|
||||
+ '<th>订单编号</th><th>来源</th><th>来源ID</th><th>金额</th><th>状态</th><th>下单时间</th>'
|
||||
+ '</tr></thead><tbody>'
|
||||
+ items.map(function (item) {
|
||||
return '<tr>'
|
||||
+ '<td data-label="订单编号"><div>' + escapeHtml(item.order_number || item.order_id || '-') + '</div><div class="muted">' + escapeHtml(item.order_id || '-') + '</div></td>'
|
||||
+ '<td data-label="来源">' + escapeHtml(item.order_source || '-') + '</td>'
|
||||
+ '<td data-label="来源ID">' + escapeHtml(item.order_source_id || '-') + '</td>'
|
||||
+ '<td data-label="金额">¥' + escapeHtml(item.order_amount || 0) + '</td>'
|
||||
+ '<td data-label="状态">' + escapeHtml(item.order_status || '-') + '</td>'
|
||||
+ '<td data-label="下单时间">' + escapeHtml(item.order_create || item.created || '-') + '</td>'
|
||||
+ '</tr>'
|
||||
}).join('')
|
||||
+ '</tbody></table></div>'
|
||||
}
|
||||
|
||||
function renderDetail() {
|
||||
const user = getSelectedUser()
|
||||
if (!user) {
|
||||
detailWrapEl.innerHTML = '<div class="empty">请选择左侧用户查看信息维护、购物车与订单详情。</div>'
|
||||
return
|
||||
}
|
||||
|
||||
detailWrapEl.innerHTML = '<div class="detail-header">'
|
||||
+ '<div>'
|
||||
+ '<h2 class="detail-title">' + escapeHtml(user.users_name || user.users_id || user.openid) + '</h2>'
|
||||
+ '<div class="detail-meta">openid:' + escapeHtml(user.openid || '-') + '</div>'
|
||||
+ '<div class="detail-meta">users_id:' + escapeHtml(user.users_id || '-') + '</div>'
|
||||
+ '<div class="detail-meta">users_idtype:' + escapeHtml(user.users_idtype || '-') + '</div>'
|
||||
+ '<div class="detail-meta">手机号:' + escapeHtml(user.users_phone || '-') + '</div>'
|
||||
+ '<div class="detail-meta">会员等级:' + escapeHtml(user.users_level_name || user.users_level || '-') + '</div>'
|
||||
+ '</div>'
|
||||
+ '</div>'
|
||||
+ renderUserProfileForm(user)
|
||||
+ '<div class="summary-grid">'
|
||||
+ '<div class="summary-card"><div class="summary-label">购物车记录数</div><div class="summary-value">' + escapeHtml(user.cart_count || 0) + '</div></div>'
|
||||
+ '<div class="summary-card"><div class="summary-label">购物车商品总数</div><div class="summary-value">' + escapeHtml(user.cart_total_quantity || 0) + '</div></div>'
|
||||
+ '<div class="summary-card"><div class="summary-label">订单数</div><div class="summary-value">' + escapeHtml(user.order_count || 0) + '</div></div>'
|
||||
+ '<div class="summary-card"><div class="summary-label">订单总金额</div><div class="summary-value">¥' + escapeHtml(user.order_total_amount || 0) + '</div></div>'
|
||||
+ '</div>'
|
||||
+ '<div class="section"><h3 class="section-title">当前购物车详情</h3>' + renderCartTable(Array.isArray(user.carts) ? user.carts : []) + '</div>'
|
||||
+ '<div class="section"><h3 class="section-title">订单记录</h3>' + renderOrderTable(Array.isArray(user.orders) ? user.orders : []) + '</div>'
|
||||
}
|
||||
|
||||
async function loadUsers() {
|
||||
setStatus('正在加载用户信息、购物车与订单数据...', '')
|
||||
try {
|
||||
const data = await requestJson('/cart-order/manage-users', {
|
||||
keyword: normalizeText(keywordInput.value),
|
||||
})
|
||||
state.users = Array.isArray(data.items) ? data.items : []
|
||||
state.userLevelOptions = Array.isArray(data.user_level_options) ? data.user_level_options : []
|
||||
if (!state.users.length) {
|
||||
state.selectedOpenid = ''
|
||||
} else if (!getSelectedUser()) {
|
||||
state.selectedOpenid = normalizeText(state.users[0].openid)
|
||||
}
|
||||
renderUserList()
|
||||
renderDetail()
|
||||
setStatus('加载完成,共 ' + state.users.length + ' 位用户。', 'success')
|
||||
} catch (err) {
|
||||
renderUserList()
|
||||
renderDetail()
|
||||
setStatus(err.message || '加载失败', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSelectedUser() {
|
||||
const user = getSelectedUser()
|
||||
if (!user) {
|
||||
setStatus('请先选择用户。', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await requestJson('/cart-order/manage-user-update', {
|
||||
openid: user.openid,
|
||||
users_name: document.getElementById('userUsersName').value,
|
||||
users_phone: document.getElementById('userUsersPhone').value,
|
||||
users_level: document.getElementById('userUsersLevel').value,
|
||||
users_type: document.getElementById('userUsersType').value,
|
||||
users_status: document.getElementById('userUsersStatus').value,
|
||||
users_rank_level: document.getElementById('userUsersRankLevel').value,
|
||||
users_auth_type: document.getElementById('userUsersAuthType').value,
|
||||
company_id: document.getElementById('userCompanyId').value,
|
||||
users_tag: document.getElementById('userUsersTag').value,
|
||||
users_parent_id: document.getElementById('userUsersParentId').value,
|
||||
users_promo_code: document.getElementById('userUsersPromoCode').value,
|
||||
usergroups_id: document.getElementById('userUsergroupsId').value,
|
||||
users_id_number: document.getElementById('userUsersIdNumber').value,
|
||||
users_picture: document.getElementById('userUsersPicture').value,
|
||||
users_id_pic_a: document.getElementById('userUsersIdPicA').value,
|
||||
users_id_pic_b: document.getElementById('userUsersIdPicB').value,
|
||||
users_title_picture: document.getElementById('userUsersTitlePicture').value,
|
||||
})
|
||||
|
||||
const updatedUser = data && data.user ? data.user : null
|
||||
if (updatedUser) {
|
||||
const index = state.users.findIndex(function (item) {
|
||||
return normalizeText(item.openid) === normalizeText(updatedUser.openid)
|
||||
})
|
||||
if (index !== -1) {
|
||||
state.users[index] = Object.assign({}, state.users[index], updatedUser)
|
||||
}
|
||||
}
|
||||
renderUserList()
|
||||
renderDetail()
|
||||
setStatus('用户信息保存成功。', 'success')
|
||||
} catch (err) {
|
||||
setStatus(err.message || '保存用户信息失败', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
userListEl.addEventListener('click', function (event) {
|
||||
const target = event.target && event.target.closest ? event.target.closest('[data-openid]') : null
|
||||
if (!target) {
|
||||
return
|
||||
}
|
||||
state.selectedOpenid = normalizeText(target.getAttribute('data-openid'))
|
||||
renderUserList()
|
||||
renderDetail()
|
||||
})
|
||||
|
||||
detailWrapEl.addEventListener('click', function (event) {
|
||||
const target = event.target
|
||||
if (target && target.id === 'saveUserBtn') {
|
||||
saveSelectedUser()
|
||||
}
|
||||
})
|
||||
|
||||
document.getElementById('searchBtn').addEventListener('click', loadUsers)
|
||||
document.getElementById('reloadBtn').addEventListener('click', loadUsers)
|
||||
document.getElementById('refreshBtn').addEventListener('click', loadUsers)
|
||||
document.getElementById('resetBtn').addEventListener('click', function () {
|
||||
keywordInput.value = ''
|
||||
loadUsers()
|
||||
})
|
||||
|
||||
loadUsers()
|
||||
</script>
|
||||
{{ template "theme_body" . }}
|
||||
</body>
|
||||
</html>
|
||||
1151
pocket-base/bai_web_pb_hooks/views/dictionary-manage.html
Normal file
1151
pocket-base/bai_web_pb_hooks/views/dictionary-manage.html
Normal file
File diff suppressed because it is too large
Load Diff
1579
pocket-base/bai_web_pb_hooks/views/document-manage.html
Normal file
1579
pocket-base/bai_web_pb_hooks/views/document-manage.html
Normal file
File diff suppressed because it is too large
Load Diff
96
pocket-base/bai_web_pb_hooks/views/index.html
Normal file
96
pocket-base/bai_web_pb_hooks/views/index.html
Normal file
@@ -0,0 +1,96 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>管理主页</title>
|
||||
<script>
|
||||
(function () {
|
||||
var token = localStorage.getItem('pb_manage_token') || ''
|
||||
var isLoggedIn = localStorage.getItem('pb_manage_logged_in') === '1'
|
||||
if (!token || !isLoggedIn) {
|
||||
window.location.replace('/pb/manage/login')
|
||||
}
|
||||
})()
|
||||
</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: 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; }
|
||||
.module + .module { margin-top: 18px; }
|
||||
.module-title { margin: 0 0 10px; font-size: 22px; color: #0f172a; }
|
||||
.grid { display: grid; grid-template-columns: repeat(3, minmax(0, 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: 14px; display: flex; justify-content: flex-start; }
|
||||
@media (max-width: 960px) {
|
||||
.grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
{{ template "theme_head" . }}
|
||||
</head>
|
||||
<body>
|
||||
<main class="wrap">
|
||||
<section class="hero">
|
||||
<h1>管理主页</h1>
|
||||
<section class="module">
|
||||
<h2 class="module-title">平台管理</h2>
|
||||
<div class="grid">
|
||||
<article class="card">
|
||||
<h2>字典管理</h2>
|
||||
<a class="btn" href="/pb/manage/dictionary-manage">进入字典管理</a>
|
||||
</article>
|
||||
<article class="card">
|
||||
<h2>文档管理</h2>
|
||||
<a class="btn" href="/pb/manage/document-manage">进入文档管理</a>
|
||||
</article>
|
||||
<article class="card">
|
||||
<h2>产品管理</h2>
|
||||
<a class="btn" href="/pb/manage/product-manage">进入产品管理</a>
|
||||
</article>
|
||||
<article class="card">
|
||||
<h2>SDK 权限管理</h2>
|
||||
<a class="btn" href="/pb/manage/sdk-permission-manage">进入权限管理</a>
|
||||
</article>
|
||||
<article class="card">
|
||||
<h2>购物车与订单</h2>
|
||||
<a class="btn" href="/pb/manage/cart-order-manage">进入用户信息及订单管理</a>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
<section class="module">
|
||||
<h2 class="module-title">AI 管理</h2>
|
||||
<div class="grid">
|
||||
<article class="card">
|
||||
<h2>AI 审计管理</h2>
|
||||
<a class="btn" href="/pb/bai-ai-manage">进入审计管理</a>
|
||||
</article>
|
||||
<article class="card">
|
||||
<h2>AI 聊天测试</h2>
|
||||
<a class="btn" href="/pb/bai-chat">进入聊天测试</a>
|
||||
</article>
|
||||
<article class="card">
|
||||
<h2>SQL 实验室</h2>
|
||||
<a class="btn" href="/pb/bai-ai-sql-lab">进入 SQL 实验室</a>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
<div class="actions">
|
||||
<button id="logoutBtn" class="btn" type="button" style="background:#dc2626;">退出登录</button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<script>
|
||||
document.getElementById('logoutBtn').addEventListener('click', function () {
|
||||
localStorage.removeItem('pb_manage_token')
|
||||
localStorage.removeItem('pb_manage_logged_in')
|
||||
localStorage.removeItem('pb_manage_login_account')
|
||||
localStorage.removeItem('pb_manage_login_time')
|
||||
window.location.replace('/pb/manage/login')
|
||||
})
|
||||
</script>
|
||||
{{ template "theme_body" . }}
|
||||
</body>
|
||||
</html>
|
||||
155
pocket-base/bai_web_pb_hooks/views/login.html
Normal file
155
pocket-base/bai_web_pb_hooks/views/login.html
Normal file
@@ -0,0 +1,155 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>登录</title>
|
||||
<script>
|
||||
(function () {
|
||||
var token = localStorage.getItem('pb_manage_token') || ''
|
||||
var isLoggedIn = localStorage.getItem('pb_manage_logged_in') === '1'
|
||||
if (token && isLoggedIn) {
|
||||
window.location.replace('/pb/manage')
|
||||
}
|
||||
})()
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
background: linear-gradient(180deg, #eff6ff 0%, #f8fafc 100%);
|
||||
color: #1f2937;
|
||||
}
|
||||
.wrap {
|
||||
max-width: 1440px;
|
||||
margin: 0 auto;
|
||||
padding: 34px 14px;
|
||||
}
|
||||
.card {
|
||||
max-width: 420px;
|
||||
margin: 0 auto;
|
||||
background: #fff;
|
||||
border: 1px solid #dbe3f0;
|
||||
border-radius: 18px;
|
||||
padding: 22px;
|
||||
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
h1 { margin-top: 0; }
|
||||
p { line-height: 1.8; color: #4b5563; margin-bottom: 20px; }
|
||||
.field { margin-bottom: 14px; }
|
||||
.field label { display: block; margin-bottom: 8px; color: #334155; font-weight: 600; }
|
||||
.field input {
|
||||
width: 100%;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 12px;
|
||||
padding: 10px 12px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.btn {
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
padding: 11px 14px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
.status { margin-top: 12px; min-height: 22px; font-size: 14px; }
|
||||
.status.error { color: #b91c1c; }
|
||||
.status.success { color: #15803d; }
|
||||
</style>
|
||||
{{ template "theme_head" . }}
|
||||
</head>
|
||||
<body>
|
||||
<main class="wrap">
|
||||
<section class="card">
|
||||
<h1>后台登录</h1>
|
||||
<p>请输入平台账号(邮箱或手机号)与密码,登录成功后会自动跳转到管理首页。</p>
|
||||
<form id="loginForm">
|
||||
<div class="field">
|
||||
<label for="account">登录账号</label>
|
||||
<input id="account" name="account" placeholder="邮箱或手机号" required />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="password">密码</label>
|
||||
<input id="password" name="password" type="password" placeholder="请输入密码" required />
|
||||
</div>
|
||||
<button class="btn" id="submitBtn" type="submit">登录</button>
|
||||
</form>
|
||||
<div id="status" class="status"></div>
|
||||
</section>
|
||||
</main>
|
||||
<script>
|
||||
(function () {
|
||||
var API_BASE = '/pb/api'
|
||||
var form = document.getElementById('loginForm')
|
||||
var accountInput = document.getElementById('account')
|
||||
var passwordInput = document.getElementById('password')
|
||||
var statusEl = document.getElementById('status')
|
||||
var submitBtn = document.getElementById('submitBtn')
|
||||
|
||||
function setStatus(message, type) {
|
||||
statusEl.textContent = message || ''
|
||||
statusEl.className = 'status' + (type ? ' ' + type : '')
|
||||
}
|
||||
|
||||
form.addEventListener('submit', async function (event) {
|
||||
event.preventDefault()
|
||||
|
||||
var loginAccount = accountInput.value.trim()
|
||||
var password = passwordInput.value
|
||||
if (!loginAccount || !password) {
|
||||
setStatus('请填写账号和密码', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
submitBtn.disabled = true
|
||||
submitBtn.textContent = '登录中...'
|
||||
setStatus('正在登录,请稍候...', '')
|
||||
|
||||
try {
|
||||
var response = await fetch(API_BASE + '/platform/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
login_account: loginAccount,
|
||||
password: password,
|
||||
}),
|
||||
})
|
||||
|
||||
var result = await response.json()
|
||||
if (!response.ok || !result || result.code >= 400) {
|
||||
throw new Error((result && result.msg) || '登录失败')
|
||||
}
|
||||
|
||||
var token = result.token || (result.data && result.data.token) || ''
|
||||
if (!token) {
|
||||
throw new Error('登录成功但未获取到 token')
|
||||
}
|
||||
|
||||
localStorage.setItem('pb_manage_token', token)
|
||||
localStorage.setItem('pb_manage_logged_in', '1')
|
||||
localStorage.setItem('pb_manage_login_account', loginAccount)
|
||||
localStorage.setItem('pb_manage_login_time', new Date().toISOString())
|
||||
|
||||
setStatus('登录成功,正在跳转...', 'success')
|
||||
window.location.replace('/pb/manage')
|
||||
} catch (error) {
|
||||
localStorage.removeItem('pb_manage_token')
|
||||
localStorage.removeItem('pb_manage_logged_in')
|
||||
setStatus(error.message || '登录失败', 'error')
|
||||
} finally {
|
||||
submitBtn.disabled = false
|
||||
submitBtn.textContent = '登录'
|
||||
}
|
||||
})
|
||||
})()
|
||||
</script>
|
||||
{{ template "theme_body" . }}
|
||||
</body>
|
||||
</html>
|
||||
2323
pocket-base/bai_web_pb_hooks/views/product-manage.html
Normal file
2323
pocket-base/bai_web_pb_hooks/views/product-manage.html
Normal file
File diff suppressed because it is too large
Load Diff
735
pocket-base/bai_web_pb_hooks/views/sdk-permission-manage.html
Normal file
735
pocket-base/bai_web_pb_hooks/views/sdk-permission-manage.html
Normal file
@@ -0,0 +1,735 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>SDK 权限管理</title>
|
||||
<script>
|
||||
(function () {
|
||||
var token = localStorage.getItem('pb_manage_token') || ''
|
||||
var isLoggedIn = localStorage.getItem('pb_manage_logged_in') === '1'
|
||||
if (!token || !isLoggedIn) {
|
||||
window.location.replace('/pb/manage/login')
|
||||
}
|
||||
})()
|
||||
</script>
|
||||
<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: #0f172a; }
|
||||
.wrap { 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, h3 { margin-top: 0; }
|
||||
p { color: #475569; line-height: 1.7; }
|
||||
.actions, .toolbar, .row-actions { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; }
|
||||
.btn { border: none; border-radius: 12px; padding: 10px 16px; font-weight: 700; cursor: pointer; text-decoration: none; display: inline-flex; align-items: center; justify-content: center; }
|
||||
.btn-primary { background: #2563eb; color: #fff; }
|
||||
.btn-light { background: #f8fafc; color: #334155; border: 1px solid #dbe3f0; }
|
||||
.btn-danger { background: #dc2626; color: #fff; }
|
||||
.btn-warning { background: #f59e0b; color: #fff; }
|
||||
.btn-success { background: #16a34a; color: #fff; }
|
||||
.status { margin-top: 12px; min-height: 24px; font-size: 14px; }
|
||||
.status.success { color: #15803d; }
|
||||
.status.error { color: #b91c1c; }
|
||||
.note { padding: 14px 16px; border-radius: 16px; background: #eff6ff; color: #1d4ed8; font-size: 14px; line-height: 1.7; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
thead { background: #eff6ff; }
|
||||
th, td { padding: 14px 12px; border-bottom: 1px solid #e5e7eb; text-align: left; vertical-align: top; }
|
||||
th { font-size: 13px; color: #475569; }
|
||||
tr:hover td { background: #f8fafc; }
|
||||
input, textarea, select { width: 100%; border: 1px solid #cbd5e1; border-radius: 12px; padding: 9px 11px; font-size: 14px; background: #fff; }
|
||||
textarea { min-height: 80px; resize: vertical; }
|
||||
.empty { text-align: center; padding: 24px 16px; color: #64748b; }
|
||||
.muted { color: #64748b; font-size: 12px; }
|
||||
.grid { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 12px; }
|
||||
.full { grid-column: 1 / -1; }
|
||||
.rule-grid { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 10px; }
|
||||
.rule-card { border: 1px solid #dbe3f0; border-radius: 16px; padding: 12px; background: #f8fbff; }
|
||||
.rule-card h4 { margin: 0 0 10px; font-size: 14px; display: flex; align-items: center; gap: 8px; }
|
||||
.rule-meta { display: flex; align-items: center; gap: 8px; margin-top: 8px; color: #475569; font-size: 12px; }
|
||||
.rule-meta input[type="checkbox"] { width: auto; margin: 0; }
|
||||
.rule-toggle { display: inline-flex; align-items: center; gap: 6px; color: #0f172a; font-size: 13px; font-weight: 600; }
|
||||
.rule-toggle input[type="checkbox"] { width: auto; margin: 0; }
|
||||
.split { display: grid; grid-template-columns: 1.15fr 1fr; gap: 14px; }
|
||||
.table-wrap { overflow: auto; }
|
||||
.collection-table { table-layout: fixed; }
|
||||
.collection-col { width: 264px; }
|
||||
.rule-col { width: calc(100% - 264px); }
|
||||
.collection-meta { display: flex; flex-direction: column; gap: 6px; align-items: flex-start; }
|
||||
.loading-mask { position: fixed; inset: 0; display: none; align-items: center; justify-content: center; padding: 24px; background: rgba(15, 23, 42, 0.42); backdrop-filter: blur(4px); z-index: 9998; }
|
||||
.loading-mask.show { display: flex; }
|
||||
.loading-card { min-width: min(92vw, 360px); padding: 24px 22px; border-radius: 20px; background: rgba(255,255,255,0.98); box-shadow: 0 28px 70px rgba(15, 23, 42, 0.22); border: 1px solid #dbe3f0; text-align: center; }
|
||||
.loading-spinner { width: 44px; height: 44px; margin: 0 auto 14px; border-radius: 999px; border: 4px solid #dbeafe; border-top-color: #2563eb; animation: sdkSpin 0.9s linear infinite; }
|
||||
.loading-text { color: #0f172a; font-size: 15px; font-weight: 700; }
|
||||
@keyframes sdkSpin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
||||
@media (max-width: 1100px) {
|
||||
.split, .rule-grid, .grid { grid-template-columns: 1fr; }
|
||||
.collection-col, .rule-col { width: auto; }
|
||||
table, thead, tbody, th, td, tr { display: block; }
|
||||
thead { display: none; }
|
||||
tr { margin-bottom: 14px; border: 1px solid #e5e7eb; border-radius: 16px; overflow: hidden; }
|
||||
td { display: flex; flex-direction: column; gap: 8px; }
|
||||
}
|
||||
</style>
|
||||
{{ template "theme_head" . }}
|
||||
</head>
|
||||
<body>
|
||||
<main class="wrap">
|
||||
<section class="panel">
|
||||
<h1>SDK 权限管理</h1>
|
||||
<p>这里管理的是 <code>tbl_auth_users</code> 用户通过 PocketBase SDK 直连数据库时的业务权限。<strong>ManagePlatform</strong> 会被视为你的业务管理员,但不会自动变成 PocketBase 原生 <code>_superusers</code>。</p>
|
||||
<div class="actions">
|
||||
<a class="btn btn-light" href="/pb/manage">返回主页</a>
|
||||
<button class="btn btn-light" id="refreshBtn" type="button">刷新数据</button>
|
||||
<button class="btn btn-success" id="syncManageBtn" type="button">同步 ManagePlatform 全权限</button>
|
||||
</div>
|
||||
<div class="status" id="status"></div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="note" id="noteBox">加载中...</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>角色管理</h2>
|
||||
<div class="grid">
|
||||
<input id="newRoleName" placeholder="角色名称" />
|
||||
<input id="newRoleCode" placeholder="角色编码,可为空" />
|
||||
<input id="newRoleStatus" type="number" placeholder="状态,默认1" value="1" />
|
||||
<button class="btn btn-primary" id="createRoleBtn" type="button">新增角色</button>
|
||||
<div class="muted">角色 ID 由系统自动生成,页面不显示。</div>
|
||||
<textarea id="newRoleRemark" class="full" placeholder="备注"></textarea>
|
||||
</div>
|
||||
<div class="table-wrap" style="margin-top:16px;">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>名称</th>
|
||||
<th>编码</th>
|
||||
<th>状态</th>
|
||||
<th>备注</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="roleTableBody">
|
||||
<tr><td colspan="5" class="empty">暂无角色。</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>用户授权</h2>
|
||||
<div class="toolbar">
|
||||
<input id="userKeywordInput" placeholder="按姓名、手机号、openid、角色搜索" />
|
||||
<button class="btn btn-light" id="searchUserBtn" type="button">查询用户</button>
|
||||
</div>
|
||||
<div class="table-wrap" style="margin-top:16px;">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>用户</th>
|
||||
<th>身份类型</th>
|
||||
<th>当前角色</th>
|
||||
<th>授权</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="userTableBody">
|
||||
<tr><td colspan="5" class="empty">暂无用户。</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>Collection 直连权限</h2>
|
||||
<div class="toolbar">
|
||||
<select id="permissionRoleSelect"></select>
|
||||
<div class="muted">这里是按“当前配置角色”逐表分配 CRUD 权限。下面依次对应“列表、详情、新增、修改、删除”五种权限,勾选后会立即保存。公开访问或自定义规则的操作不会显示授权勾选框。</div>
|
||||
</div>
|
||||
<div class="table-wrap" style="margin-top:16px;">
|
||||
<table class="collection-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="collection-col">集合</th>
|
||||
<th class="rule-col">当前角色权限</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="collectionTableBody">
|
||||
<tr><td colspan="2" class="empty">暂无集合。</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<div class="loading-mask" id="loadingMask">
|
||||
<div class="loading-card">
|
||||
<div class="loading-spinner"></div>
|
||||
<div class="loading-text" id="loadingText">处理中,请稍候...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API_BASE = '/pb/api/sdk-permission'
|
||||
const tokenKey = 'pb_manage_token'
|
||||
const statusEl = document.getElementById('status')
|
||||
const noteBox = document.getElementById('noteBox')
|
||||
const roleTableBody = document.getElementById('roleTableBody')
|
||||
const userTableBody = document.getElementById('userTableBody')
|
||||
const collectionTableBody = document.getElementById('collectionTableBody')
|
||||
const permissionRoleSelect = document.getElementById('permissionRoleSelect')
|
||||
const loadingMask = document.getElementById('loadingMask')
|
||||
const loadingText = document.getElementById('loadingText')
|
||||
const loadingState = { count: 0 }
|
||||
const state = {
|
||||
roles: [],
|
||||
users: [],
|
||||
collections: [],
|
||||
userKeyword: '',
|
||||
selectedPermissionRoleId: '',
|
||||
}
|
||||
|
||||
function setStatus(message, type) {
|
||||
statusEl.textContent = message || ''
|
||||
statusEl.className = 'status' + (type ? ' ' + type : '')
|
||||
}
|
||||
|
||||
function showLoading(message) {
|
||||
loadingState.count += 1
|
||||
loadingText.textContent = message || '处理中,请稍候...'
|
||||
loadingMask.classList.add('show')
|
||||
}
|
||||
|
||||
function hideLoading() {
|
||||
loadingState.count = Math.max(0, loadingState.count - 1)
|
||||
if (!loadingState.count) {
|
||||
loadingMask.classList.remove('show')
|
||||
loadingText.textContent = '处理中,请稍候...'
|
||||
}
|
||||
}
|
||||
|
||||
function getToken() {
|
||||
return localStorage.getItem(tokenKey) || ''
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value || '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
function getRoleById(roleId) {
|
||||
const target = String(roleId || '')
|
||||
return state.roles.find(function (role) {
|
||||
return role.role_id === target
|
||||
}) || null
|
||||
}
|
||||
|
||||
function getRoleName(roleId) {
|
||||
const role = getRoleById(roleId)
|
||||
return role ? role.role_name : ''
|
||||
}
|
||||
|
||||
function syncSelectedPermissionRole() {
|
||||
const exists = state.roles.some(function (role) {
|
||||
return role.role_id === state.selectedPermissionRoleId
|
||||
})
|
||||
if (!exists) {
|
||||
state.selectedPermissionRoleId = state.roles.length ? state.roles[0].role_id : ''
|
||||
}
|
||||
}
|
||||
|
||||
function decodeErrMsg(value) {
|
||||
if (!value) {
|
||||
return ''
|
||||
}
|
||||
|
||||
try {
|
||||
return decodeURIComponent(value)
|
||||
} catch (_err) {
|
||||
return String(value)
|
||||
}
|
||||
}
|
||||
|
||||
function unwrapApiResponse(data, res) {
|
||||
const safe = data && typeof data === 'object' ? data : {}
|
||||
const headerCode = Number(res && res.headers ? (res.headers.get('X-Status-Code') || 0) : 0)
|
||||
const headerErrMsg = decodeErrMsg(res && res.headers ? res.headers.get('X-Err-Msg') : '')
|
||||
const code = Number(safe.statusCode || safe.code || headerCode || 0)
|
||||
const errMsg = safe.errMsg || safe.msg || headerErrMsg || ''
|
||||
const hasLegacyEnvelope = Object.prototype.hasOwnProperty.call(safe, 'data')
|
||||
&& (Object.prototype.hasOwnProperty.call(safe, 'code')
|
||||
|| Object.prototype.hasOwnProperty.call(safe, 'statusCode')
|
||||
|| Object.prototype.hasOwnProperty.call(safe, 'msg')
|
||||
|| Object.prototype.hasOwnProperty.call(safe, 'errMsg'))
|
||||
const payload = hasLegacyEnvelope ? (safe.data || {}) : safe
|
||||
return { code: code, errMsg: errMsg, payload: payload }
|
||||
}
|
||||
|
||||
async function requestJson(path, payload) {
|
||||
const token = getToken()
|
||||
if (!token) {
|
||||
localStorage.removeItem('pb_manage_logged_in')
|
||||
window.location.replace('/pb/manage/login')
|
||||
throw new Error('登录状态已失效,请重新登录')
|
||||
}
|
||||
|
||||
const res = await fetch(API_BASE + path, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: 'Bearer ' + token,
|
||||
},
|
||||
body: JSON.stringify(payload || {}),
|
||||
})
|
||||
|
||||
const data = await res.json()
|
||||
const unwrapped = unwrapApiResponse(data, res)
|
||||
if (!res.ok || !data || unwrapped.code >= 400) {
|
||||
if (res.status === 401 || res.status === 403 || unwrapped.code === 401 || unwrapped.code === 403) {
|
||||
localStorage.removeItem('pb_manage_token')
|
||||
localStorage.removeItem('pb_manage_logged_in')
|
||||
localStorage.removeItem('pb_manage_login_account')
|
||||
localStorage.removeItem('pb_manage_login_time')
|
||||
window.location.replace('/pb/manage/login')
|
||||
}
|
||||
throw new Error(unwrapped.errMsg || '请求失败')
|
||||
}
|
||||
|
||||
return unwrapped.payload
|
||||
}
|
||||
|
||||
function roleOptionsHtml(selectedRoleId) {
|
||||
const current = String(selectedRoleId || '')
|
||||
return ['<option value="">未分配</option>'].concat(state.roles.map(function (role) {
|
||||
return '<option value="' + escapeHtml(role.role_id) + '"' + (current === role.role_id ? ' selected' : '') + '>' + escapeHtml(role.role_name) + '</option>'
|
||||
})).join('')
|
||||
}
|
||||
|
||||
function renderRoles() {
|
||||
if (!state.roles.length) {
|
||||
roleTableBody.innerHTML = '<tr><td colspan="5" class="empty">暂无角色。</td></tr>'
|
||||
return
|
||||
}
|
||||
|
||||
roleTableBody.innerHTML = state.roles.map(function (role) {
|
||||
return '<tr data-role-id="' + escapeHtml(role.role_id) + '">'
|
||||
+ '<td><input data-role-field="role_name" value="' + escapeHtml(role.role_name) + '" /></td>'
|
||||
+ '<td><input data-role-field="role_code" value="' + escapeHtml(role.role_code) + '" /></td>'
|
||||
+ '<td><input data-role-field="role_status" type="number" value="' + escapeHtml(role.role_status) + '" /></td>'
|
||||
+ '<td><textarea data-role-field="role_remark">' + escapeHtml(role.role_remark) + '</textarea></td>'
|
||||
+ '<td><div class="row-actions"><button class="btn btn-light" type="button" onclick="window.__saveRoleRow(\'' + encodeURIComponent(role.role_id) + '\')">保存</button><button class="btn btn-danger" type="button" onclick="window.__deleteRoleRow(\'' + encodeURIComponent(role.role_id) + '\')">删除</button></div></td>'
|
||||
+ '</tr>'
|
||||
}).join('')
|
||||
}
|
||||
|
||||
function renderUsers() {
|
||||
if (!state.users.length) {
|
||||
userTableBody.innerHTML = '<tr><td colspan="5" class="empty">暂无匹配用户。</td></tr>'
|
||||
return
|
||||
}
|
||||
|
||||
userTableBody.innerHTML = state.users.map(function (user) {
|
||||
const name = user.users_name || '未命名用户'
|
||||
const phone = user.users_phone || '无手机号'
|
||||
const roleName = user.role_name || getRoleName(user.usergroups_id) || '未分配'
|
||||
return '<tr data-user-id="' + escapeHtml(user.pb_id) + '">'
|
||||
+ '<td><div><strong>' + escapeHtml(name) + '</strong></div><div class="muted">' + escapeHtml(phone) + '</div><div class="muted">' + escapeHtml(user.openid) + '</div></td>'
|
||||
+ '<td><div>' + escapeHtml(user.users_idtype || '') + '</div><div class="muted">' + escapeHtml(user.users_type || '') + '</div></td>'
|
||||
+ '<td>' + escapeHtml(roleName) + '</td>'
|
||||
+ '<td><select data-user-role-select="1">' + roleOptionsHtml(user.usergroups_id) + '</select></td>'
|
||||
+ '<td><button class="btn btn-light" type="button" onclick="window.__saveUserRole(\'' + escapeHtml(user.pb_id) + '\')">保存角色</button></td>'
|
||||
+ '</tr>'
|
||||
}).join('')
|
||||
}
|
||||
|
||||
function renderPermissionRoleOptions() {
|
||||
if (!state.roles.length) {
|
||||
permissionRoleSelect.innerHTML = '<option value="">暂无角色</option>'
|
||||
permissionRoleSelect.disabled = true
|
||||
return
|
||||
}
|
||||
|
||||
permissionRoleSelect.disabled = false
|
||||
permissionRoleSelect.innerHTML = state.roles.map(function (role) {
|
||||
return '<option value="' + escapeHtml(role.role_id) + '"' + (role.role_id === state.selectedPermissionRoleId ? ' selected' : '') + '>' + escapeHtml(role.role_name) + '</option>'
|
||||
}).join('')
|
||||
}
|
||||
|
||||
function getOperationLabel(operation) {
|
||||
const map = {
|
||||
list: '列表',
|
||||
view: '详情',
|
||||
create: '新增',
|
||||
update: '修改',
|
||||
delete: '删除',
|
||||
}
|
||||
return map[operation] || operation
|
||||
}
|
||||
|
||||
function getRuleSummary(config, selectedRoleId) {
|
||||
const current = config || { mode: 'locked', includeManagePlatform: false, roles: [], rawExpression: '' }
|
||||
const items = []
|
||||
if (current.mode === 'public') items.push('公开可访问')
|
||||
if (current.mode === 'authenticated') items.push('登录用户可访问')
|
||||
if (current.includeManagePlatform) items.push('含 ManagePlatform')
|
||||
if (current.mode === 'custom') items.push('自定义规则')
|
||||
if (Array.isArray(current.roles) && current.roles.length) {
|
||||
items.push('已分配角色数:' + current.roles.length)
|
||||
}
|
||||
return items.length ? items.join(',') : '当前无额外说明'
|
||||
}
|
||||
|
||||
function canControlRule(config) {
|
||||
const current = config || { mode: 'locked', includeManagePlatform: false, roles: [], rawExpression: '' }
|
||||
return !!state.selectedPermissionRoleId && current.mode !== 'custom' && current.mode !== 'public'
|
||||
}
|
||||
|
||||
function isCollectionFullyChecked(collection) {
|
||||
if (!collection || !collection.parsedRules) {
|
||||
return false
|
||||
}
|
||||
|
||||
const operations = ['list', 'view', 'create', 'update', 'delete']
|
||||
let controllableCount = 0
|
||||
let checkedCount = 0
|
||||
|
||||
for (let i = 0; i < operations.length; i += 1) {
|
||||
const config = collection.parsedRules[operations[i]]
|
||||
if (!canControlRule(config)) {
|
||||
continue
|
||||
}
|
||||
controllableCount += 1
|
||||
if (config && Array.isArray(config.roles) && config.roles.indexOf(state.selectedPermissionRoleId) !== -1) {
|
||||
checkedCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
return controllableCount > 0 && controllableCount === checkedCount
|
||||
}
|
||||
|
||||
function getCollectionControllableCount(collection) {
|
||||
if (!collection || !collection.parsedRules) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const operations = ['list', 'view', 'create', 'update', 'delete']
|
||||
let controllableCount = 0
|
||||
|
||||
for (let i = 0; i < operations.length; i += 1) {
|
||||
if (canControlRule(collection.parsedRules[operations[i]])) {
|
||||
controllableCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
return controllableCount
|
||||
}
|
||||
|
||||
function renderRuleCard(collectionName, operation, config) {
|
||||
const current = config || { mode: 'locked', includeManagePlatform: false, roles: [], rawExpression: '' }
|
||||
const selectedRoleId = state.selectedPermissionRoleId
|
||||
const checked = selectedRoleId && Array.isArray(current.roles) && current.roles.indexOf(selectedRoleId) !== -1
|
||||
const canControl = canControlRule(current)
|
||||
const summary = current.mode === 'public'
|
||||
? '可公开访问'
|
||||
: getRuleSummary(current, selectedRoleId)
|
||||
return '<div class="rule-card" data-collection="' + escapeHtml(collectionName) + '" data-op="' + operation + '">'
|
||||
+ '<h4>' + (canControl ? '<label class="rule-toggle"><input type="checkbox" data-rule-field="allowSelectedRole"' + (checked ? ' checked' : '') + ' /></label>' : '') + '<span>' + getOperationLabel(operation) + '</span></h4>'
|
||||
+ '<div class="muted" style="margin-top:8px;">' + escapeHtml(summary) + '</div>'
|
||||
+ (current.mode === 'custom' ? '<div class="muted" style="margin-top:8px;">当前操作使用 custom 规则,禁止修改</div>' : '')
|
||||
+ '</div>'
|
||||
}
|
||||
|
||||
function renderCollections() {
|
||||
if (!state.collections.length) {
|
||||
collectionTableBody.innerHTML = '<tr><td colspan="2" class="empty">暂无可管理集合。</td></tr>'
|
||||
return
|
||||
}
|
||||
|
||||
collectionTableBody.innerHTML = state.collections.map(function (collection) {
|
||||
const allChecked = isCollectionFullyChecked(collection)
|
||||
const controllableCount = getCollectionControllableCount(collection)
|
||||
return '<tr data-collection-row="' + escapeHtml(collection.name) + '">'
|
||||
+ '<td class="collection-col"><div class="collection-meta"><div><strong>' + escapeHtml(collection.name) + '</strong></div><div class="muted">' + escapeHtml(collection.type) + '</div><label class="rule-toggle"><input type="checkbox" data-rule-field="toggleCollection"' + (allChecked ? ' checked' : '') + (state.selectedPermissionRoleId && controllableCount > 0 ? '' : ' disabled') + ' /><span>全选</span></label></div></td>'
|
||||
+ '<td><div class="rule-grid">'
|
||||
+ renderRuleCard(collection.name, 'list', collection.parsedRules.list)
|
||||
+ renderRuleCard(collection.name, 'view', collection.parsedRules.view)
|
||||
+ renderRuleCard(collection.name, 'create', collection.parsedRules.create)
|
||||
+ renderRuleCard(collection.name, 'update', collection.parsedRules.update)
|
||||
+ renderRuleCard(collection.name, 'delete', collection.parsedRules.delete)
|
||||
+ '</div></td>'
|
||||
+ '</tr>'
|
||||
}).join('')
|
||||
}
|
||||
|
||||
async function loadContext() {
|
||||
showLoading('正在加载权限管理数据...')
|
||||
setStatus('正在加载权限管理数据...', '')
|
||||
try {
|
||||
const data = await requestJson('/context', { keyword: state.userKeyword })
|
||||
state.roles = Array.isArray(data.roles) ? data.roles : []
|
||||
state.users = Array.isArray(data.users) ? data.users : []
|
||||
state.collections = Array.isArray(data.collections) ? data.collections : []
|
||||
syncSelectedPermissionRole()
|
||||
noteBox.textContent = data.note || '权限管理说明已加载。'
|
||||
renderRoles()
|
||||
renderUsers()
|
||||
renderPermissionRoleOptions()
|
||||
renderCollections()
|
||||
setStatus('权限管理数据已刷新。', 'success')
|
||||
} catch (err) {
|
||||
setStatus(err.message || '加载权限管理数据失败', 'error')
|
||||
} finally {
|
||||
hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
function getRoleRowPayload(roleId) {
|
||||
const targetId = decodeURIComponent(roleId)
|
||||
const row = roleTableBody.querySelector('[data-role-id="' + targetId.replace(/"/g, '\\"') + '"]')
|
||||
if (!row) {
|
||||
throw new Error('未找到对应角色行')
|
||||
}
|
||||
|
||||
const find = function (fieldName) {
|
||||
const input = row.querySelector('[data-role-field="' + fieldName + '"]')
|
||||
return input ? input.value : ''
|
||||
}
|
||||
|
||||
return {
|
||||
original_role_id: targetId,
|
||||
role_name: find('role_name'),
|
||||
role_code: find('role_code'),
|
||||
role_status: find('role_status'),
|
||||
role_remark: find('role_remark'),
|
||||
}
|
||||
}
|
||||
|
||||
function getRuleBoxConfig(collectionName, operation) {
|
||||
const collection = state.collections.find(function (item) {
|
||||
return item.name === collectionName
|
||||
})
|
||||
const current = collection && collection.parsedRules ? collection.parsedRules[operation] : null
|
||||
const base = current || {
|
||||
mode: 'locked',
|
||||
includeManagePlatform: false,
|
||||
roles: [],
|
||||
rawExpression: '',
|
||||
}
|
||||
if (base.mode === 'custom') {
|
||||
return {
|
||||
mode: 'custom',
|
||||
includeManagePlatform: !!base.includeManagePlatform,
|
||||
roles: Array.isArray(base.roles) ? base.roles.slice() : [],
|
||||
rawExpression: base.rawExpression || '',
|
||||
}
|
||||
}
|
||||
|
||||
const box = collectionTableBody.querySelector('.rule-card[data-collection="' + collectionName.replace(/"/g, '\\"') + '"][data-op="' + operation + '"]')
|
||||
const allowEl = box ? box.querySelector('[data-rule-field="allowSelectedRole"]') : null
|
||||
const roles = Array.isArray(base.roles) ? base.roles.slice() : []
|
||||
const selectedRoleId = state.selectedPermissionRoleId
|
||||
const nextRoles = roles.filter(function (roleId) {
|
||||
return roleId !== selectedRoleId
|
||||
})
|
||||
|
||||
if (selectedRoleId && allowEl && allowEl.checked && nextRoles.indexOf(selectedRoleId) === -1) {
|
||||
nextRoles.push(selectedRoleId)
|
||||
}
|
||||
|
||||
let nextMode = base.mode
|
||||
if (nextMode === 'locked' && nextRoles.length) {
|
||||
nextMode = 'roleBased'
|
||||
} else if (nextMode === 'roleBased' && !nextRoles.length && !base.includeManagePlatform) {
|
||||
nextMode = 'locked'
|
||||
}
|
||||
|
||||
return {
|
||||
mode: nextMode,
|
||||
includeManagePlatform: !!base.includeManagePlatform,
|
||||
roles: nextRoles,
|
||||
rawExpression: base.rawExpression || '',
|
||||
}
|
||||
}
|
||||
|
||||
async function createRole() {
|
||||
const payload = {
|
||||
role_name: document.getElementById('newRoleName').value.trim(),
|
||||
role_code: document.getElementById('newRoleCode').value.trim(),
|
||||
role_status: document.getElementById('newRoleStatus').value.trim() || '1',
|
||||
role_remark: document.getElementById('newRoleRemark').value.trim(),
|
||||
}
|
||||
|
||||
showLoading('正在新增角色...')
|
||||
try {
|
||||
await requestJson('/role-save', payload)
|
||||
document.getElementById('newRoleName').value = ''
|
||||
document.getElementById('newRoleCode').value = ''
|
||||
document.getElementById('newRoleStatus').value = '1'
|
||||
document.getElementById('newRoleRemark').value = ''
|
||||
await loadContext()
|
||||
setStatus('角色新增成功。', 'success')
|
||||
} catch (err) {
|
||||
setStatus(err.message || '新增角色失败', 'error')
|
||||
} finally {
|
||||
hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
async function saveRoleRow(roleId) {
|
||||
showLoading('正在保存角色...')
|
||||
try {
|
||||
await requestJson('/role-save', getRoleRowPayload(roleId))
|
||||
await loadContext()
|
||||
setStatus('角色保存成功。', 'success')
|
||||
} catch (err) {
|
||||
setStatus(err.message || '保存角色失败', 'error')
|
||||
} finally {
|
||||
hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteRoleRow(roleId) {
|
||||
const targetId = decodeURIComponent(roleId)
|
||||
if (!window.confirm('确认删除角色「' + targetId + '」吗?这会清空绑定该角色的用户,并从已解析的集合规则中移除它。')) {
|
||||
return
|
||||
}
|
||||
|
||||
showLoading('正在删除角色...')
|
||||
try {
|
||||
await requestJson('/role-delete', { role_id: targetId })
|
||||
await loadContext()
|
||||
setStatus('角色删除成功。', 'success')
|
||||
} catch (err) {
|
||||
setStatus(err.message || '删除角色失败', 'error')
|
||||
} finally {
|
||||
hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
async function saveUserRole(pbId) {
|
||||
const row = userTableBody.querySelector('[data-user-id="' + pbId.replace(/"/g, '\\"') + '"]')
|
||||
const select = row ? row.querySelector('[data-user-role-select="1"]') : null
|
||||
const payload = {
|
||||
pb_id: pbId,
|
||||
usergroups_id: select ? select.value : '',
|
||||
}
|
||||
|
||||
showLoading('正在保存用户角色...')
|
||||
try {
|
||||
await requestJson('/user-role-update', payload)
|
||||
await loadContext()
|
||||
setStatus('用户角色已更新。', 'success')
|
||||
} catch (err) {
|
||||
setStatus(err.message || '更新用户角色失败', 'error')
|
||||
} finally {
|
||||
hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
async function saveCollectionRules(collectionName) {
|
||||
const targetName = decodeURIComponent(collectionName)
|
||||
if (!state.selectedPermissionRoleId) {
|
||||
setStatus('请先选择一个要配置权限的角色。', 'error')
|
||||
return
|
||||
}
|
||||
const payload = {
|
||||
collection_name: targetName,
|
||||
rules: {
|
||||
list: getRuleBoxConfig(targetName, 'list'),
|
||||
view: getRuleBoxConfig(targetName, 'view'),
|
||||
create: getRuleBoxConfig(targetName, 'create'),
|
||||
update: getRuleBoxConfig(targetName, 'update'),
|
||||
delete: getRuleBoxConfig(targetName, 'delete'),
|
||||
},
|
||||
}
|
||||
|
||||
showLoading('正在同步集合权限...')
|
||||
try {
|
||||
await requestJson('/collection-save', payload)
|
||||
await loadContext()
|
||||
setStatus('已保存角色「' + (getRoleName(state.selectedPermissionRoleId) || '未命名角色') + '」在集合「' + targetName + '」上的权限。', 'success')
|
||||
} catch (err) {
|
||||
setStatus(err.message || '保存集合权限失败', 'error')
|
||||
} finally {
|
||||
hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
async function syncManagePlatform() {
|
||||
if (!window.confirm('确认将 ManagePlatform 同步为所有业务集合的全权限吗?这不会创建 _superusers,但会为业务表开放全部 CRUD。')) {
|
||||
return
|
||||
}
|
||||
|
||||
showLoading('正在同步 ManagePlatform 全权限...')
|
||||
try {
|
||||
const data = await requestJson('/manageplatform-sync', {})
|
||||
await loadContext()
|
||||
setStatus('已同步 ManagePlatform 全权限,共处理 ' + String((data && data.count) || 0) + ' 个集合。', 'success')
|
||||
} catch (err) {
|
||||
setStatus(err.message || '同步 ManagePlatform 全权限失败', 'error')
|
||||
} finally {
|
||||
hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
window.__saveRoleRow = saveRoleRow
|
||||
window.__deleteRoleRow = deleteRoleRow
|
||||
window.__saveUserRole = saveUserRole
|
||||
window.__saveCollectionRules = saveCollectionRules
|
||||
|
||||
document.getElementById('createRoleBtn').addEventListener('click', createRole)
|
||||
document.getElementById('refreshBtn').addEventListener('click', loadContext)
|
||||
document.getElementById('syncManageBtn').addEventListener('click', syncManagePlatform)
|
||||
permissionRoleSelect.addEventListener('change', function () {
|
||||
state.selectedPermissionRoleId = permissionRoleSelect.value || ''
|
||||
renderPermissionRoleOptions()
|
||||
renderCollections()
|
||||
})
|
||||
collectionTableBody.addEventListener('change', function (event) {
|
||||
const target = event.target
|
||||
if (!target) {
|
||||
return
|
||||
}
|
||||
|
||||
const field = target.getAttribute('data-rule-field')
|
||||
if (field === 'allowSelectedRole') {
|
||||
const box = target.closest('.rule-card')
|
||||
if (!box) {
|
||||
return
|
||||
}
|
||||
const collectionName = box.getAttribute('data-collection') || ''
|
||||
saveCollectionRules(collectionName)
|
||||
return
|
||||
}
|
||||
|
||||
if (field === 'toggleCollection') {
|
||||
const row = target.closest('[data-collection-row]')
|
||||
if (!row) {
|
||||
return
|
||||
}
|
||||
const checkboxes = row.querySelectorAll('[data-rule-field="allowSelectedRole"]')
|
||||
for (let i = 0; i < checkboxes.length; i += 1) {
|
||||
if (!checkboxes[i].disabled) {
|
||||
checkboxes[i].checked = !!target.checked
|
||||
}
|
||||
}
|
||||
const collectionName = row.getAttribute('data-collection-row') || ''
|
||||
saveCollectionRules(collectionName)
|
||||
}
|
||||
})
|
||||
document.getElementById('searchUserBtn').addEventListener('click', function () {
|
||||
state.userKeyword = document.getElementById('userKeywordInput').value.trim()
|
||||
loadContext()
|
||||
})
|
||||
|
||||
loadContext()
|
||||
</script>
|
||||
{{ template "theme_body" . }}
|
||||
</body>
|
||||
</html>
|
||||
@@ -229,6 +229,9 @@ components:
|
||||
users_level:
|
||||
type: string
|
||||
description: "用户等级"
|
||||
users_level_name:
|
||||
type: string
|
||||
description: "用户等级名称,按 `users_level -> 数据-会员等级` 字典描述实时解析"
|
||||
users_tag:
|
||||
type: string
|
||||
description: "用户标签"
|
||||
@@ -293,6 +296,7 @@ components:
|
||||
users_phone: 手机号 | string
|
||||
users_phone_masked: 手机号脱敏值 | string
|
||||
users_level: 用户等级 | string
|
||||
users_level_name: 用户等级名称 | string
|
||||
users_tag: 用户标签 | string
|
||||
users_picture: 用户头像附件ID | string
|
||||
users_picture_url: 用户头像文件流链接 | string
|
||||
@@ -1230,6 +1234,7 @@ paths:
|
||||
创建平台用户 auth record。
|
||||
服务端会自动生成 GUID 并写入统一身份字段 `openid`,同时写入 `users_idtype = ManagePlatform`。
|
||||
前端以 `users_phone + password/passwordConfirm` 注册,但服务端仍会创建 PocketBase 原生 auth 用户。
|
||||
首次注册创建时,`users_level` 默认保持为空,不自动写入会员等级。
|
||||
注册成功后直接返回 PocketBase 原生 auth token。
|
||||
requestBody:
|
||||
required: true
|
||||
@@ -1260,6 +1265,7 @@ paths:
|
||||
前端使用平台注册时保存的 `邮箱或手机号 + password` 登录。
|
||||
仅允许 `users_idtype = ManagePlatform` 的用户通过该接口登录。
|
||||
服务端会根据 `login_account` 自动判断邮箱或手机号,并定位平台用户,再使用该用户的 PocketBase 原生 identity(当前为 `email`)执行原生 password auth。
|
||||
返回体中的 `user.users_level_name` 为服务端按“数据-会员等级”字典实时解析后的等级名称。
|
||||
登录成功后直接返回 PocketBase 原生 auth token。
|
||||
requestBody:
|
||||
required: true
|
||||
@@ -1803,6 +1809,3 @@ paths:
|
||||
description: "非 ManagePlatform 用户无权访问"
|
||||
'415':
|
||||
description: "请求体必须为 application/json"
|
||||
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -205,6 +205,9 @@ components:
|
||||
users_level:
|
||||
type: string
|
||||
description: 用户等级
|
||||
users_level_name:
|
||||
type: string
|
||||
description: 用户等级名称,按 `users_level -> 数据-会员等级` 字典描述实时解析
|
||||
users_tag:
|
||||
type: string
|
||||
description: 用户标签
|
||||
@@ -269,6 +272,7 @@ components:
|
||||
users_phone: 手机号 | string
|
||||
users_phone_masked: 手机号脱敏值 | string
|
||||
users_level: 用户等级 | string
|
||||
users_level_name: 用户等级名称 | string
|
||||
users_tag: 用户标签 | string
|
||||
users_picture: 用户头像附件ID | string
|
||||
users_picture_url: 用户头像文件流链接 | string
|
||||
@@ -1249,6 +1253,7 @@ paths:
|
||||
使用微信 code 换取微信侧 openid,并写入统一身份字段 `tbl_auth_users.openid`。
|
||||
若 `tbl_auth_users` 中不存在对应用户则自动创建 auth record,随后返回 PocketBase 原生 auth token。
|
||||
首次注册创建时会写入 `users_idtype = WeChat`。
|
||||
首次注册创建时,`users_level` 默认保持为空,不自动写入会员等级。
|
||||
返回的 `token` 可直接用于 PocketBase SDK 与当前 hooks 接口调用。
|
||||
requestBody:
|
||||
required: true
|
||||
@@ -1281,6 +1286,7 @@ paths:
|
||||
创建平台用户 auth record。
|
||||
服务端会自动生成 GUID 并写入统一身份字段 `openid`,同时写入 `users_idtype = ManagePlatform`。
|
||||
前端以 `users_phone + password/passwordConfirm` 注册,但服务端仍会创建 PocketBase 原生 auth 用户。
|
||||
首次注册创建时,`users_level` 默认保持为空,不自动写入会员等级。
|
||||
注册成功后直接返回 PocketBase 原生 auth token。
|
||||
requestBody:
|
||||
required: true
|
||||
@@ -1311,6 +1317,7 @@ paths:
|
||||
前端使用平台注册时保存的 `邮箱或手机号 + password` 登录。
|
||||
仅允许 `users_idtype = ManagePlatform` 的用户通过该接口登录。
|
||||
服务端会根据 `login_account` 自动判断邮箱或手机号,并定位平台用户,再使用该用户的 PocketBase 原生 identity(当前为 `email`)执行原生 password auth。
|
||||
返回体中的 `user.users_level_name` 为服务端按“数据-会员等级”字典实时解析后的等级名称。
|
||||
登录成功后直接返回 PocketBase 原生 auth token。
|
||||
requestBody:
|
||||
required: true
|
||||
@@ -1882,5 +1889,3 @@ paths:
|
||||
description: 非 ManagePlatform 用户无权访问
|
||||
'415':
|
||||
description: 请求体必须为 application/json
|
||||
|
||||
|
||||
|
||||
149
script/add-product-function-field.js
Normal file
149
script/add-product-function-field.js
Normal file
@@ -0,0 +1,149 @@
|
||||
import { createRequire } from 'module';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
let runtimeConfig = {};
|
||||
try {
|
||||
runtimeConfig = require('../pocket-base/bai_api_pb_hooks/bai_api_shared/config/runtime.js');
|
||||
} catch (_error) {
|
||||
runtimeConfig = {};
|
||||
}
|
||||
|
||||
function readEnvFile(filePath) {
|
||||
if (!fs.existsSync(filePath)) return {};
|
||||
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const result = {};
|
||||
|
||||
for (const rawLine of content.split(/\r?\n/)) {
|
||||
const line = rawLine.trim();
|
||||
if (!line || line.startsWith('#')) continue;
|
||||
|
||||
const index = line.indexOf('=');
|
||||
if (index === -1) continue;
|
||||
|
||||
const key = line.slice(0, index).trim();
|
||||
const value = line.slice(index + 1).trim();
|
||||
result[key] = value;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const backendEnv = readEnvFile(path.resolve(__dirname, '..', 'back-end', '.env'));
|
||||
const pbUrl = String(
|
||||
process.env.PB_URL
|
||||
|| backendEnv.POCKETBASE_API_URL
|
||||
|| runtimeConfig.POCKETBASE_API_URL
|
||||
|| 'http://127.0.0.1:8090'
|
||||
).replace(/\/+$/, '');
|
||||
const authToken = process.env.POCKETBASE_AUTH_TOKEN || runtimeConfig.POCKETBASE_AUTH_TOKEN || '';
|
||||
|
||||
if (!authToken) {
|
||||
console.error('❌ 缺少 POCKETBASE_AUTH_TOKEN,无法执行字段迁移。');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function normalizeFieldPayload(field, targetSpec) {
|
||||
const payload = {
|
||||
name: targetSpec && targetSpec.name ? targetSpec.name : field.name,
|
||||
type: targetSpec && targetSpec.type ? targetSpec.type : field.type,
|
||||
};
|
||||
|
||||
if (field && field.id) {
|
||||
payload.id = field.id;
|
||||
}
|
||||
|
||||
if (typeof field.required !== 'undefined') {
|
||||
payload.required = !!field.required;
|
||||
}
|
||||
|
||||
if (payload.type === 'autodate') {
|
||||
payload.onCreate = typeof field.onCreate === 'boolean' ? field.onCreate : true;
|
||||
payload.onUpdate = typeof field.onUpdate === 'boolean' ? field.onUpdate : false;
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const pb = new PocketBase(pbUrl);
|
||||
pb.authStore.save(authToken, null);
|
||||
|
||||
console.log(`🔄 开始处理字段迁移,PocketBase: ${pbUrl}`);
|
||||
|
||||
const collections = await pb.collections.getFullList({ sort: '-created' });
|
||||
const collection = collections.find((item) => item.name === 'tbl_product_list');
|
||||
|
||||
if (!collection) {
|
||||
throw new Error('未找到集合 tbl_product_list');
|
||||
}
|
||||
|
||||
const targetFieldName = 'prod_list_function';
|
||||
const targetFieldType = 'json';
|
||||
const existingField = (collection.fields || []).find((field) => field.name === targetFieldName);
|
||||
|
||||
if (existingField && existingField.type === targetFieldType) {
|
||||
console.log('✅ 字段已存在且类型正确,无需变更。');
|
||||
console.log('✅ 校验完成: tbl_product_list.prod_list_function (json)');
|
||||
return;
|
||||
}
|
||||
|
||||
const nextFields = [];
|
||||
let patched = false;
|
||||
|
||||
for (let i = 0; i < (collection.fields || []).length; i += 1) {
|
||||
const field = collection.fields[i];
|
||||
if (field.name === targetFieldName) {
|
||||
nextFields.push(normalizeFieldPayload(field, { name: targetFieldName, type: targetFieldType }));
|
||||
patched = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
nextFields.push(normalizeFieldPayload(field));
|
||||
}
|
||||
|
||||
if (!patched) {
|
||||
nextFields.push({
|
||||
name: targetFieldName,
|
||||
type: targetFieldType,
|
||||
required: false,
|
||||
});
|
||||
}
|
||||
|
||||
await pb.collections.update(collection.id, {
|
||||
name: collection.name,
|
||||
type: collection.type,
|
||||
listRule: collection.listRule,
|
||||
viewRule: collection.viewRule,
|
||||
createRule: collection.createRule,
|
||||
updateRule: collection.updateRule,
|
||||
deleteRule: collection.deleteRule,
|
||||
fields: nextFields,
|
||||
indexes: collection.indexes || [],
|
||||
});
|
||||
|
||||
const updated = await pb.collections.getOne(collection.id);
|
||||
const verifiedField = (updated.fields || []).find((field) => field.name === targetFieldName);
|
||||
|
||||
if (!verifiedField || verifiedField.type !== targetFieldType) {
|
||||
throw new Error('字段写入后校验失败:prod_list_function 未成功写入 json 类型');
|
||||
}
|
||||
|
||||
console.log('✅ 字段迁移成功: tbl_product_list.prod_list_function (json)');
|
||||
}
|
||||
|
||||
run().catch((error) => {
|
||||
console.error('❌ 迁移失败:', {
|
||||
status: error && error.status,
|
||||
message: error && error.message,
|
||||
response: error && error.response,
|
||||
});
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -84,7 +84,7 @@
|
||||
| users_id_number | text | 证件号 |
|
||||
| users_phone | text | 用户电话号码 |
|
||||
| users_wx_openid | text | 微信号 |
|
||||
| users_level | text | 用户等级 |
|
||||
| users_level | text | 用户等级枚举值(新用户默认空) |
|
||||
| users_type | text | 用户类型 |
|
||||
| users_tag | text | 用户标签 |
|
||||
| users_status | text | 用户状态 |
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"init:dictionary": "node pocketbase.dictionary.js",
|
||||
"migrate:file-fields": "node pocketbase.file-fields-to-attachments.js",
|
||||
"migrate:product-params-array": "node migrate-product-parameters-to-array.js",
|
||||
"migrate:add-product-function-field": "node add-product-function-field.js",
|
||||
"test:company-native-api": "node test-tbl-company-native-api.js",
|
||||
"test:company-owner-sync": "node test-company-owner-sync.js"
|
||||
},
|
||||
|
||||
@@ -38,6 +38,7 @@ const collections = [
|
||||
{ name: 'prod_list_description', type: 'text' },
|
||||
{ name: 'prod_list_feature', type: 'text' },
|
||||
{ name: 'prod_list_parameters', type: 'json' },
|
||||
{ name: 'prod_list_function', type: 'json' },
|
||||
{ name: 'prod_list_plantype', type: 'text' },
|
||||
{ name: 'prod_list_category', type: 'text', required: true },
|
||||
{ name: 'prod_list_sort', type: 'number' },
|
||||
@@ -47,6 +48,7 @@ const collections = [
|
||||
{ name: 'prod_list_tags', type: 'text' },
|
||||
{ name: 'prod_list_status', type: 'text' },
|
||||
{ name: 'prod_list_basic_price', type: 'number' },
|
||||
{ name: 'prod_list_vip_price', type: 'json' },
|
||||
{ name: 'prod_list_remark', type: 'text' },
|
||||
],
|
||||
indexes: [
|
||||
|
||||
Reference in New Issue
Block a user