diff --git a/b/bill/finance_settlement_create.dspy b/b/bill/finance_settlement_create.dspy index cdb281d..35568a9 100644 --- a/b/bill/finance_settlement_create.dspy +++ b/b/bill/finance_settlement_create.dspy @@ -147,7 +147,7 @@ async def _fetch_source_rows(sor, args): AND sd.accounting_orgid=${accounting_orgid}$ AND sd.del_flg='0' AND sd.accounting_dir='贷' - AND sd.subjectname LIKE '待结转%' + AND sd.subjectname LIKE '待结转%%' """ else: filters.append('cust.parentid=${counterparty_orgid}$') diff --git a/b/bill/finance_settlement_preview.dspy b/b/bill/finance_settlement_preview.dspy index 131cb36..6089fc6 100644 --- a/b/bill/finance_settlement_preview.dspy +++ b/b/bill/finance_settlement_preview.dspy @@ -161,7 +161,7 @@ async def _fetch_source_rows(sor, args): AND sd.accounting_orgid=${accounting_orgid}$ AND sd.del_flg='0' AND sd.accounting_dir='贷' - AND sd.subjectname LIKE '待结转%' + AND sd.subjectname LIKE '待结转%%' """ settlement_expr = 'COALESCE(sd.amount, 0)' sale_mode_expr = 'sd.subjectname' diff --git a/b/bill/finance_settlement_summary.dspy b/b/bill/finance_settlement_summary.dspy index 9cf4987..feaf468 100644 --- a/b/bill/finance_settlement_summary.dspy +++ b/b/bill/finance_settlement_summary.dspy @@ -91,7 +91,7 @@ async def _fetch_source_rows(sor, args): AND bd.accounting_orgid = ${accounting_orgid}$ AND bd.del_flg = '0' AND bd.accounting_dir = '贷' - AND bd.subjectname LIKE '待结转%' + AND bd.subjectname LIKE '待结转%%' """ if args.get('counterparty_orgid'): filters.append('b.providerid = ${counterparty_orgid}$') diff --git a/b/product/add_user_inquiry.dspy b/b/product/add_user_inquiry.dspy index 88e0670..e09b03f 100644 --- a/b/product/add_user_inquiry.dspy +++ b/b/product/add_user_inquiry.dspy @@ -20,7 +20,11 @@ async def add_user_inquiry(ns={}): 'name': ns.get('name'), 'phone': ns.get('phone'), 'company': ns.get('company'), - 'email': ns.get('email') + 'enterprise_type': ns.get('enterprise_type'), + 'region': ns.get('region'), + 'consult_direction': ns.get('consult_direction'), + 'email': ns.get('email'), + 'source': ns.get('source') } await sor.C('product_inquiry', ns_c) return { diff --git a/b/product/delete_user_inquiry.dspy b/b/product/delete_user_inquiry.dspy index eecdbb5..415735f 100644 --- a/b/product/delete_user_inquiry.dspy +++ b/b/product/delete_user_inquiry.dspy @@ -1,13 +1,37 @@ async def delete_user_inquiry(ns={}): db = DBPools() async with db.sqlorContext('kboss') as sor: - ns_c = { - 'id': ns.get('id') - } - await sor.D('product_inquiry', ns_c) + ids = ns.get('ids') or ns.get('id') + if not ids: + return { + 'status': False, + 'msg': '请传递id' + } + if isinstance(ids, str): + if '[' in ids: + ids = ids.replace("'", '"') + ids = json.loads(ids) + elif ',' in ids: + ids = ids.replace('"', '').replace("'", '').split(',') + else: + ids = [ids] + + delete_count = 0 + for inquiry_id in ids: + if not inquiry_id: + continue + ns_c = { + 'id': inquiry_id + } + await sor.D('product_inquiry', ns_c) + delete_count += 1 + return { 'status': True, - 'msg': 'delete success' + 'msg': 'delete success', + 'data': { + 'delete_count': delete_count + } } ret = await delete_user_inquiry(params_kw) diff --git a/b/product/search_user_inquiry.dspy b/b/product/search_user_inquiry.dspy index 906e50e..a5ce76d 100644 --- a/b/product/search_user_inquiry.dspy +++ b/b/product/search_user_inquiry.dspy @@ -10,8 +10,72 @@ async def search_user_inquiry(ns={}): db = DBPools() async with db.sqlorContext('kboss') as sor: - search_sql = """select * from product_inquiry where domain_name = '%s' and del_flg = '0' order by update_time desc;""" % domain_name + where_conditions = ["domain_name = '%s'" % domain_name, "del_flg = '0'"] + if ns.get('name'): + where_conditions.append("name like '%%%%%s%%%%'" % ns.get('name')) + if ns.get('phone'): + where_conditions.append("phone like '%%%%%s%%%%'" % ns.get('phone')) + if ns.get('email'): + where_conditions.append("email like '%%%%%s%%%%'" % ns.get('email')) + if ns.get('source'): + if ns.get('source') == '未知': + where_conditions.append("(source is null or source = '')") + else: + where_conditions.append("source = '%s'" % ns.get('source')) + if ns.get('feedback'): + where_conditions.append("feedback = '%s'" % ns.get('feedback')) + where_clause = ' and '.join(where_conditions) + + # 分页参数 + page = int(ns.get('page', 1)) + page_size = int(ns.get('page_size', 20)) + offset = (page - 1) * page_size + + # 统计查询(基于全部符合条件的数据) + count_sql = """select count(*) as cnt from product_inquiry where %s""" % where_clause + total_count = (await sor.sqlExe(count_sql, {}))[0]['cnt'] + + source_sql = """select source, count(*) as cnt from product_inquiry where %s group by source""" % where_clause + source_result = await sor.sqlExe(source_sql, {}) + source_stats = {} + for row in source_result: + src = row.get('source') or '未知' + source_stats[src] = row.get('cnt') + + pending_sql = """select count(*) as cnt from product_inquiry where %s and feedback = '0'""" % where_clause + pending_count = (await sor.sqlExe(pending_sql, {}))[0]['cnt'] + + source_list_sql = """select distinct source from product_inquiry where %s""" % where_clause + source_list = [row['source'] for row in (await sor.sqlExe(source_list_sql, {}))] + has_empty_source = any(s is None or s == '' for s in source_list) + source_list = [s for s in source_list if s] # 过滤空值 + if has_empty_source: + source_list.append('未知') + + # 分页查询 + search_sql = """select * from product_inquiry where %s order by update_time desc limit %d offset %d;""" % (where_clause, page_size, offset) result = await sor.sqlExe(search_sql, {}) + dict_sql = """select dict_type, dict_key, dict_value from product_inquiry_dict where status = 1 order by dict_type asc, sort_order asc;""" + dict_result = await sor.sqlExe(dict_sql, {}) + dict_mapping = {} + for dict_item in dict_result: + dict_type = dict_item.get('dict_type') + dict_mapping.setdefault(dict_type, {})[str(dict_item.get('dict_key'))] = dict_item.get('dict_value') + + value_mapping = { + 'custom_type': {'0': '个人', '1': '企业'}, + 'enterprise_type': dict_mapping.get('enterprise_type', {}), + 'region': dict_mapping.get('region', {}), + 'feedback': {'0': '待回复', '1': '已回复'} + } + for data_dic in result: + for key, mapping in value_mapping.items(): + if key in data_dic: + data_dic['%s_name' % key] = mapping.get(str(data_dic.get(key)), data_dic.get(key)) + direction_mapping = dict_mapping.get('direction', {}) + direction_keys = str(data_dic.get('consult_direction')).split(',') if data_dic.get('consult_direction') else [] + data_dic['consult_direction_name'] = ','.join([direction_mapping.get(direction_key.strip(), direction_key.strip()) for direction_key in direction_keys if direction_key.strip()]) + if ns.get('to_excel') == '1': # 创建映射字段 导出execl # 结果转换成 中文名称:值 的字典列表 @@ -21,14 +85,15 @@ async def search_user_inquiry(ns={}): 'phone': '联系人电话', 'email': '邮箱', 'company': '公司名称', + 'enterprise_type': '企业类型', + 'region': '所在区域', + 'consult_direction': '咨询方向', 'content': '咨询内容', 'feedback': '反馈状态', + 'remark': '备注', + 'create_at': '创建时间' } # 新增值映射字典,集中管理各字段的数值转换规则 - value_mapping = { - 'custom_type': {'0': '个人', '1': '企业'}, - 'feedback': {'0': '未反馈', '1': '已反馈'} # 根据表结构补充反馈状态映射 - } # 转换字典键为中文 for data_dic in result: # 拆分后:显式循环结构(便于后续处理) @@ -40,7 +105,11 @@ async def search_user_inquiry(ns={}): continue value = data_dic[key] chinese_key = field_mapping[key] - if key in value_mapping: + if key == 'consult_direction': + direction_mapping = dict_mapping.get('direction', {}) + direction_keys = str(value).split(',') if value else [] + new_data_dic[chinese_key] = ','.join([direction_mapping.get(direction_key.strip(), direction_key.strip()) for direction_key in direction_keys if direction_key.strip()]) + elif key in value_mapping: mapped_value = value_mapping[key].get(str(value), value) # 若未找到对应映射,保留原始值 new_data_dic[chinese_key] = mapped_value else: @@ -51,7 +120,14 @@ async def search_user_inquiry(ns={}): return { 'status': True, 'msg': 'search success', - 'data': result + 'data': result, + 'total_count': total_count, + 'source_stats': source_stats, + 'pending_count': pending_count, + 'source_list': source_list, + 'page': page, + 'page_size': page_size, + 'feedback_list': [{'id': 0, 'name': '待回复'},{'id': 1, 'name': '已回复'}] } ret = await search_user_inquiry(params_kw) diff --git a/b/product/search_user_inquiry_dict.dspy b/b/product/search_user_inquiry_dict.dspy new file mode 100644 index 0000000..610e410 --- /dev/null +++ b/b/product/search_user_inquiry_dict.dspy @@ -0,0 +1,16 @@ +async def search_user_inquiry_dict(ns={}): + db = DBPools() + async with db.sqlorContext('kboss') as sor: + where_sql = "where status = 1" + if ns.get('dict_type'): + where_sql += " and dict_type = '%s'" % ns.get('dict_type') + search_sql = """select id, dict_type, dict_key, dict_value, sort_order from product_inquiry_dict %s order by dict_type asc, sort_order asc;""" % where_sql + result = await sor.sqlExe(search_sql, {}) + return { + 'status': True, + 'msg': 'search success', + 'data': result + } + +ret = await search_user_inquiry_dict(params_kw) +return ret diff --git a/b/product/update_user_inquiry.dspy b/b/product/update_user_inquiry.dspy index 9e43c5f..e3a9cbc 100644 --- a/b/product/update_user_inquiry.dspy +++ b/b/product/update_user_inquiry.dspy @@ -1,14 +1,40 @@ async def update_user_inquiry(ns={}): db = DBPools() async with db.sqlorContext('kboss') as sor: - ns_c = { - 'id': ns.get('id'), - 'feedback': ns.get('feedback') - } - await sor.U('product_inquiry', ns_c) + ids = ns.get('ids') or ns.get('id') + if not ids: + return { + 'status': False, + 'msg': '请传递id' + } + if isinstance(ids, str): + if ids.startswith('['): + ids = json.loads(ids) + elif ',' in ids: + ids = ids.replace('"', '').replace("'", '').split(',') + else: + ids = [ids] + + update_count = 0 + for inquiry_id in ids: + if not inquiry_id: + continue + ns_c = { + 'id': inquiry_id + } + if 'feedback' in ns: + ns_c['feedback'] = ns.get('feedback') + if 'remark' in ns: + ns_c['remark'] = ns.get('remark') + await sor.U('product_inquiry', ns_c) + update_count += 1 + return { 'status': True, - 'msg': 'update success' + 'msg': 'update success', + 'data': { + 'update_count': update_count + } } ret = await update_user_inquiry(params_kw) diff --git a/b/user_inquiry.txt b/b/user_inquiry.txt new file mode 100644 index 0000000..a594ec0 --- /dev/null +++ b/b/user_inquiry.txt @@ -0,0 +1,43 @@ +CREATE TABLE `product_inquiry` ( + `id` varchar(32) NOT NULL COMMENT '唯一标识符', + `domain_name` varchar(64) NOT NULL COMMENT '所属域名', + `publish_type` varchar(1) DEFAULT NULL COMMENT '发布商品1/ 发布需求2', + `relate_id` varchar(32) DEFAULT NULL COMMENT '发布商品1/ 发布需求2', + `content` varchar(1024) DEFAULT NULL COMMENT '咨询需求内容', + `custom_type` tinyint(1) DEFAULT NULL COMMENT '客户类型(0-个人/1-企业)', + `name` varchar(50) DEFAULT NULL COMMENT '联系人姓名', + `phone` varchar(20) DEFAULT NULL COMMENT '联系电话', + `company` varchar(100) DEFAULT NULL COMMENT '企业客户公司名称', + `enterprise_type` tinyint(1) DEFAULT NULL COMMENT '企业类型(1-大型/2-中小企业/3-OPC个人/4-高校科研机构/5-政府/6-其他)', + `region` tinyint(1) DEFAULT NULL COMMENT '所在区域(0-大陆/1-港澳台)', + `consult_direction` varchar(20) DEFAULT NULL COMMENT '咨询方向(多选用逗号分隔:1,2,3)', + `email` varchar(50) DEFAULT NULL COMMENT '电子邮箱', + `feedback` varchar(1) DEFAULT '0' COMMENT '反馈状态', + `del_flg` varchar(1) DEFAULT '0' COMMENT '删除标志(0-正常/1-已删除)', + `update_time` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp() COMMENT '更新时间', + `create_at` timestamp NULL DEFAULT current_timestamp() COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC COMMENT='产品咨询表'; + +CREATE TABLE `product_inquiry_dict` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `dict_type` varchar(30) NOT NULL COMMENT '字典类型(标识属于哪一组选项)', + `dict_key` tinyint(4) NOT NULL COMMENT '字典键值(对应数据库实际存储的数字)', + `dict_value` varchar(50) NOT NULL COMMENT '字典显示名称(前端下拉框展示的文字)', + `sort_order` int(11) DEFAULT 0 COMMENT '排序序号(数字越小越靠前)', + `status` tinyint(1) DEFAULT 1 COMMENT '状态(0-禁用/1-启用)', + `create_time` timestamp NOT NULL DEFAULT current_timestamp() COMMENT '创建时间', + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='咨询表字典配置表(运营可维护)'; +product_inquiry_dict表相关数据: +1 enterprise_type 1 大型企业 1 1 2026-07-07 16:32:45 +2 enterprise_type 2 中小企业 2 1 2026-07-07 16:32:45 +3 enterprise_type 3 OPC个人 3 1 2026-07-07 16:32:45 +4 enterprise_type 4 高校科研机构 4 1 2026-07-07 16:32:45 +5 enterprise_type 5 政府 5 1 2026-07-07 16:32:45 +6 enterprise_type 6 其他 6 1 2026-07-07 16:32:45 +7 region 0 大陆 1 1 2026-07-07 16:34:02 +8 region 1 港澳台 2 1 2026-07-07 16:34:02 +9 direction 1 AI Infra 基础设施(云/网/算) 1 1 2026-07-07 16:34:02 +10 direction 2 AI Agent 智能体(产品开发) 2 1 2026-07-07 16:34:02 +11 direction 3 AI Builder 炼智师(能力提升培训) 3 1 2026-07-07 16:34:02 \ No newline at end of file diff --git a/docs/财务结算中心.html b/docs/财务结算中心.html new file mode 100644 index 0000000..fec7c97 --- /dev/null +++ b/docs/财务结算中心.html @@ -0,0 +1,800 @@ + + + + + + 财务结算中心 + + + +
+
+
+ Finance Settlement Console +

财务结算中心

+

+ 面向供应商与分销商的日结、月结费用查询和结算处理页面。支持汇总查询、账单预览、结算单创建、审批提交、审批回调和结算单追踪。 +

+
+
+
+ 接口状态 + Ready +
+
+ 当前模式 + 供应商 / 日结 +
+
+
+ +
+
+
销售金额
+
0.00000000
+
sales_amount
+
+
+
结算金额
+
0.00000000
+
settlement_amount
+
+
+
平台收入
+
0.00000000
+
platform_income_amount
+
+
+
账单数量
+
0
+
bill_count
+
+
+ +
+ + +
+
+

结算数据

+ 等待查询 +
+ +
+ + +
+ +
+ + + + + + + + + + + +
提示
请先执行查询
+
+ + +
+
+
+ + + + diff --git a/f/web-kboss/package.json b/f/web-kboss/package.json index c8d672e..8aa16f7 100644 --- a/f/web-kboss/package.json +++ b/f/web-kboss/package.json @@ -14,7 +14,10 @@ "new": "plop", "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml", "test:unit": "jest --clearCache && vue-cli-service test:unit", - "test:ci": "npm run lint && npm run test:unit" + "test:ci": "npm run lint && npm run test:unit", + "i18n:extract": "node scripts/i18n-extract.js --dir src/views/homePage", + "i18n:extract:all": "node scripts/i18n-extract.js --dir src", + "i18n:replace:home": "node scripts/i18n-extract.js --dir src/views/homePage --replace" }, "dependencies": { "@form-create/element-ui": "^2.5.30", @@ -55,6 +58,7 @@ "vue-count-to": "^1.0.13", "vue-cropper": "^0.6.5", "vue-device-detector": "^1.1.6", + "vue-i18n": "^8.28.2", "vue-infinite-scroll": "^2.0.2", "vue-router": "^3.0.2", "vue-splitpane": "1.0.4", @@ -87,7 +91,7 @@ "eslint-plugin-vue": "6.2.2", "html-webpack-plugin": "3.2.0", "husky": "1.3.1", - "less": "^3.9.0", + "less": "^3.13.1", "less-loader": "^4.1.0", "lint-staged": "8.1.5", "mockjs": "1.0.1-beta3", diff --git a/f/web-kboss/scripts/i18n-extract.js b/f/web-kboss/scripts/i18n-extract.js new file mode 100644 index 0000000..f8fbc81 --- /dev/null +++ b/f/web-kboss/scripts/i18n-extract.js @@ -0,0 +1,191 @@ +/* eslint-disable no-console */ +const fs = require('fs') +const path = require('path') + +const projectRoot = process.cwd() +const args = process.argv.slice(2) + +const getArg = (name, defaultValue = '') => { + const full = `--${name}` + const hit = args.find((item) => item.startsWith(`${full}=`)) + if (hit) return hit.slice(full.length + 1) + const idx = args.indexOf(full) + if (idx !== -1 && args[idx + 1]) return args[idx + 1] + return defaultValue +} + +const hasFlag = (flag) => args.includes(`--${flag}`) + +const targetDirArg = getArg('dir', 'src/views/homePage') +const replaceMode = hasFlag('replace') + +const targetDir = path.resolve(projectRoot, targetDirArg) +const zhAutoPath = path.resolve(projectRoot, 'src/i18n/lang/zh-CN.auto.json') +const enAutoPath = path.resolve(projectRoot, 'src/i18n/lang/en-US.auto.json') + +const chinesePattern = /[\u4e00-\u9fa5]/ +const ignorePattern = /^(\s*|[-:,.(){}\[\]/\\]+)$/ + +const readJsonSafe = (filePath) => { + if (!fs.existsSync(filePath)) return {} + try { + return JSON.parse(fs.readFileSync(filePath, 'utf8')) + } catch (error) { + console.warn(`[warn] JSON parse failed: ${filePath}`) + return {} + } +} + +const writeJson = (filePath, value) => { + const content = JSON.stringify(value, null, 2) + '\n' + fs.writeFileSync(filePath, content, 'utf8') +} + +const walkFiles = (dir, bucket) => { + const entries = fs.readdirSync(dir, { withFileTypes: true }) + entries.forEach((entry) => { + const fullPath = path.join(dir, entry.name) + if (entry.isDirectory()) { + if (['node_modules', '.git', 'dist'].includes(entry.name)) return + walkFiles(fullPath, bucket) + return + } + if (!/\.(vue|js)$/.test(entry.name)) return + bucket.push(fullPath) + }) +} + +const normalizeText = (text) => + text + .replace(/\s+/g, ' ') + .replace(/ /g, ' ') + .trim() + +const keyFromText = (text) => { + let hash = 0 + for (let i = 0; i < text.length; i += 1) { + hash = (hash * 131 + text.charCodeAt(i)) >>> 0 + } + return `auto.k_${hash.toString(16)}` +} + +const zhMessages = readJsonSafe(zhAutoPath) +const enMessages = readJsonSafe(enAutoPath) + +const reverseMap = new Map() +Object.keys(zhMessages).forEach((key) => { + reverseMap.set(zhMessages[key], key) +}) + +const ensureKey = (rawText) => { + const text = normalizeText(rawText) + if (!text || !chinesePattern.test(text) || ignorePattern.test(text)) return null + + if (reverseMap.has(text)) return reverseMap.get(text) + + let key = keyFromText(text) + while (zhMessages[key] && zhMessages[key] !== text) { + key = `${key}_${Math.floor(Math.random() * 10000)}` + } + + zhMessages[key] = text + if (!Object.prototype.hasOwnProperty.call(enMessages, key)) { + enMessages[key] = '' + } + reverseMap.set(text, key) + return key +} + +const transformTemplate = (template) => { + let replacedCount = 0 + + const textNodeRegex = />([^<>{}\n]*[\u4e00-\u9fa5][^<>{}\n]*) { + const key = ensureKey(inner) + if (!key || !replaceMode) return full + replacedCount += 1 + return `>{{ $t('${key}') }}<` + }) + + const attrRegex = /\s([a-zA-Z_][\w-]*)=(["'])([^"']*[\u4e00-\u9fa5][^"']*)\2/g + updated = updated.replace(attrRegex, (full, attr, quote, value) => { + const key = ensureKey(value) + if (!key || !replaceMode) return full + replacedCount += 1 + return ` :${attr}="$t('${key}')"` + }) + + return { updated, replacedCount } +} + +const processVueFile = (filePath) => { + const raw = fs.readFileSync(filePath, 'utf8') + const templateMatch = raw.match(/