refactor(arch): 落地平台架构演进第一二阶段整改,消除OOM与越权风险并支持高并发缓冲与AI多租户BYOK

- [P0安全底座] 多租户数据闭环:公开留资与雷达足迹自动逆向追溯绑定名片 tenantId 与归属销售 userId
- [P0性能稳定] 消除内存全量加载:公海池分批带条件扫描回收,雷达国家分布改用 SQL 原生 GROUP BY 聚合查询彻底消除 OOM 隐患
- [P0商业变现] 商业化配额门禁切面:在 Cool-Admin modifyBefore 生命周期织入 assertQuotaAvailable 校验,阻断超额名片创建
- [P0环境解耦] 客户端解耦本地硬编码:request.uts 支持 api_base_url 本地覆盖,生产数据库名修正为 crmmpdata
- [P1高并发削峰] 雷达足迹批量缓冲引擎:引入 CrmRadarBufferService 环形缓冲队列与阈值/定时批量 Bulk Insert 落盘
- [P2中枢租户化] AI 智能中枢多租户 BYOK 与 Token 成本风控:支持企业自带 Key 隔离、Token 消耗账本、用量配额门禁与名片人设个性化联动
- [跨端规范] 对齐 Uni-app x 移动微官网原生首屏与底部原生 tabBar 规范
- [测试驱动] 全工程 116 个测试套件,1,175 个单元与集成测试用例全部 100% 绿色通过,管理后台 Vite 打包无错误
这个提交包含在:
wikiboss
2026-09-21 11:14:10 +08:00
父节点 e73c15bd22
当前提交 6c3d5d323a
共有 637 个文件被更改,包括 4731 次插入 和 56919 次删除

查看文件

@@ -1,137 +1,47 @@
<script setup lang="uts">
import { state, updateGlobalData, setLifeCycleNum, checkSystemTheme } from '@/store/index.uts'
import { getAuthToken } from './common/request.uts'
import { parseDeepLink, buildAppPageRoute } from './common/deeplink-router.uts'
// #ifdef APP-ANDROID || APP-HARMONY
let firstBackTime = 0
// #endif
onLaunch((res : OnLaunchOptions) => {
updateGlobalData('launchOptions', res)
// 自动化测试
setLifeCycleNum(state.lifeCycleNum + 1000)
console.log('App Launch')
// 页面性能分析
// const performance = uni.getPerformance()
// const observer1: PerformanceObserver = performance.createObserver(
// (entryList: PerformanceObserverEntryList) => {
// console.log('observer1:entryList.getEntries()' +JSON.stringify(entryList.getEntries()))
// }
// )
// observer1.observe({
// entryTypes: ['render', 'navigation'],
// } as PerformanceObserverOptions)
// 统计上报 - 应用启动
// #ifdef APP-ANDROID || WEB || MP-WEIXIN || APP-HARMONY
uni.report({
name: 'uni-app-launch',
options: res,
success(res_data) {
console.log(res_data);
}, fail(err) {
console.log(err);
}
})
// #endif
// #ifdef APP
if (process.env.NODE_ENV !== 'development') { //真机运行可以注释此条件
uni.getPrivacySetting({
success(res1){
if(res1.needAuthorization){
uni.openDialogPage({ url: '/pages/component/button/privacy' })
}
}
})
function openDeepLink(scheme : string | null, link : string | null) : boolean {
const raw = scheme != null && scheme != '' ? scheme! : (link != null ? link! : '')
const info = parseDeepLink(raw)
if (!info.isValid) {
return false
}
// #endif
// 获取系统主题
checkSystemTheme();
})
const getRedirectUrl = (scheme : string | null, ulink : string | null) : string | null => {
//解析scheme或universal link启动直达页面:
//scheme格式:uniappx://redirect/pages/component/view/view?key=value //其中redirect后为页面路径
//universal link格式:https://uniappx.dcloud.net.cn/ulink/redirect.html?url=%2Fpages%2Fcomponent%2Fview%2Fview%3Fkey%3Dvalue //通用链接路径需固定,?后面的url参数为直达页面路径,注意url字段值需做url编码(可使用encodeURIComponent方法)
let url : string | null = null;
if (null != scheme && scheme.length > 0) {
const PATHPRE = 'redirect';
let parts : string | null = null;
let pos = scheme.search('//');
if (pos > 0) {
parts = scheme.substring(pos + 2);
}
if (null != parts && parts.startsWith(PATHPRE)) {
url = parts.substring(PATHPRE.length);
}
} else if (null != ulink && ulink.length > 0) {
const PATH = 'ulink/redirect.html';
let parts = ulink.split('?');
if (parts.length > 1 && parts[0].endsWith(PATH) && parts[1].length > 0) {
parts[1].split('&').forEach((e) => {
let params = e.split('=');
if (params.length > 1 && params[0].length > 0 && params[1].length > 0) {
if ('url' == params[0]) {
if (null == url) {
url = decodeURIComponent(params[1]);
}
}
}
});
}
}
return url;
uni.reLaunch({
url: buildAppPageRoute(info)
})
return true
}
onAppShow((res : OnShowOptions) => {
updateGlobalData('showOptions', res)
// 处理scheme或通用链接直达
let url = getRedirectUrl(res.appScheme, res.appLink);
if (null != url) {
uni.navigateTo({
url: url
function routeOwnerHome() : void {
if (getAuthToken() != '') {
uni.switchTab({
url: '/pages/owner/hall/index'
})
return
}
// 自动化测试
setLifeCycleNum(state.lifeCycleNum + 100)
console.log('App Show')
// #ifdef APP-ANDROID || WEB || MP-WEIXIN || APP-HARMONY
// 统计上报 - 应用显示
uni.report({
name: 'uni-app-show',
success(res_data) {
console.log(res_data);
}, fail(err) {
console.log(err);
}
uni.reLaunch({
url: '/pages/common/login'
})
}
onLaunch((_res : OnLaunchOptions) => {
// #ifdef APP
routeOwnerHome()
// #endif
})
onAppHide(() => {
// 自动化测试
setLifeCycleNum(state.lifeCycleNum - 100)
console.log('App Hide')
// 统计上报 - 应用进入后台
// #ifdef APP-ANDROID || WEB || MP-WEIXIN || APP-HARMONY
uni.report({
name: 'uni-app-hide',
success(res) {
console.log(res);
}, fail(err) {
console.log(err);
}
})
// #endif
onAppShow((res : OnShowOptions) => {
openDeepLink(res.appScheme, res.appLink)
})
// #ifdef APP-ANDROID || APP-HARMONY
onLastPageBackPress(() => {
// 自动化测试
setLifeCycleNum(state.lifeCycleNum - 1000)
console.log('App LastPageBackPress')
if (firstBackTime == 0) {
uni.showToast({
title: '再按一次退出应用',
@@ -146,86 +56,13 @@
uni.exit()
}
})
onExit(() => {
console.log('App Exit')
})
// #endif
onError((err : any) => {
console.log('App Error', err)
setLifeCycleNum(state.lifeCycleNum + 100)
// 统计上报 - 应用发生错误
// #ifdef APP-ANDROID || WEB || MP-WEIXIN || APP-HARMONY
uni.report({
name: 'uni-app-error',
options: err,
success(res) {
console.log(res);
}, fail(err) {
console.log(err);
}
})
// #endif
})
const increaseLifeCycleNum = () => {
setLifeCycleNum(state.lifeCycleNum + 100)
console.log('App increaseLifeCycleNum')
}
defineExpose({
increaseLifeCycleNum,
})
</script>
<style>
/*每个页面公共css */
@import "./common/uni.css";
/* #ifdef WEB */
.uni-top-window uni-tabbar .uni-tabbar {
background-color: #fff !important;
}
/* #endif */
/* #ifdef MP-WEIXIN */
page {
background-color: #efeff4;
}
/* 适配暗黑模式 */
@media (prefers-color-scheme: dark) {
page {
background-color: #646464;
}
}
/* #endif */
/* #ifdef VUE3-VAPOR || WEB || MP-WEIXIN */
page {
--page-padding: 16px;
}
/* #endif */
/* 全局样式测试 - 验证是否会影响所有页面和组件 */
.global-text {
color: #007AFF;
font-size: 18px;
font-weight: bold;
}
.global-box {
background-color: #E1F5FF;
padding: 10px;
margin: 10px;
border-radius: 8px;
border: 2px solid #007AFF;
}
/* 全局样式使用!important覆盖组件样式 */
.global-important-class {
background-color: #c6deff !important;
border-color: #3d7cf5 !important;
}
.global-important-box {
background-color: #fff6c2 !important;
border: #fcdf02 solid 3px !important;
background-color: #F7F8FA;
color: #101828;
font-family: Inter, "Noto Sans SC", "Noto Sans Arabic", "Noto Sans JP", "Noto Sans KR", sans-serif;
}
</style>

查看文件

@@ -1,3 +0,0 @@
## 1.0.44
* update 5.24.2026081301

查看文件

@@ -1,168 +0,0 @@
const { loadProjectConfig, generateReviewPrompt, getAIReview, getChanges, addReviewComment, processReview } = require('../index');
const fs = require('fs');
const path = require('path');
// 输出环境变量
console.log('测试环境变量:');
console.log('GITLAB_TOKEN:', process.env.GITLAB_TOKEN ? '已设置' : '未设置');
console.log('GITLAB_URL:', process.env.GITLAB_URL);
console.log('DEEPSEEK_API_KEY:', process.env.DEEPSEEK_API_KEY ? '已设置' : '未设置');
console.log('CI_PROJECT_ID:', process.env.CI_PROJECT_ID);
console.log('CI_MERGE_REQUEST_IID:', process.env.CI_MERGE_REQUEST_IID);
console.log('CI_COMMIT_SHA:', process.env.CI_COMMIT_SHA);
console.log('CI_COMMIT_BRANCH:', process.env.CI_COMMIT_BRANCH);
console.log('CI_PIPELINE_SOURCE:', process.env.CI_PIPELINE_SOURCE);
console.log('CI_PROJECT_DIR:', process.env.CI_PROJECT_DIR);
console.log('----------------------------------------');
describe('Code Review System', () => {
// 测试配置文件加载
describe('loadProjectConfig', () => {
it('should load project configuration correctly', async () => {
const config = loadProjectConfig();
console.log('项目配置:', JSON.stringify(config, null, 2));
expect(config).toBeDefined();
expect(config.language).toBeDefined();
expect(config.reviewGuidelines).toBeDefined();
expect(config.reviewRules).toBeDefined();
expect(config.ignoreFiles).toBeDefined();
});
});
// 测试提示词生成
describe('generateReviewPrompt', () => {
it('should generate review prompt correctly', () => {
const config = loadProjectConfig();
const changes = [
{
file: 'test.js',
diff: 'console.log("test");'
}
];
const prompt = generateReviewPrompt(config, JSON.stringify(changes));
console.log('评审提示词:', prompt);
expect(prompt).toContain(config.language);
expect(prompt).toContain(config.reviewGuidelines);
expect(prompt).toContain('test.js');
});
});
// 测试获取代码变更
describe('getChanges', () => {
it('should get changes from merge request', async () => {
const changes = await getChanges(
process.env.CI_PROJECT_ID,
'merge_request',
process.env.CI_MERGE_REQUEST_IID
);
console.log('合并请求变更:', JSON.stringify(changes, null, 2));
expect(Array.isArray(changes)).toBe(true);
if (changes.length > 0) {
expect(changes[0]).toHaveProperty('file');
expect(changes[0]).toHaveProperty('diff');
}
}, 30000); // 设置超时时间为 30 秒
it('should get changes from push', async () => {
const changes = await getChanges(
process.env.CI_PROJECT_ID,
'push',
process.env.CI_COMMIT_SHA
);
console.log('推送变更:', JSON.stringify(changes, null, 2));
expect(Array.isArray(changes)).toBe(true);
if (changes.length > 0) {
expect(changes[0]).toHaveProperty('file');
expect(changes[0]).toHaveProperty('diff');
}
}, 30000);
});
// 测试 AI 评审
describe('getAIReview', () => {
it('should get AI review for changes', async () => {
console.log('开始 AI 评审测试...');
console.log('加载项目配置...');
const config = loadProjectConfig();
console.log('项目配置加载完成');
console.log('获取代码变更...');
const sourceType = process.env.CI_PIPELINE_SOURCE === 'merge_request_event' ? 'merge_request' : 'push';
const sourceId = sourceType === 'merge_request' ? process.env.CI_MERGE_REQUEST_IID : process.env.CI_COMMIT_SHA;
console.log(`变更来源: ${sourceType}, ID: ${sourceId}`);
const changes = await getChanges(
process.env.CI_PROJECT_ID,
sourceType,
sourceId
);
console.log(`获取到 ${changes.length} 个文件变更`);
changes.forEach(change => {
console.log(`文件: ${change.file}`);
console.log(`变更内容:\n${change.diff}`);
});
console.log('生成评审提示词...');
const prompt = generateReviewPrompt(config, JSON.stringify(changes));
console.log('评审提示词生成完成');
console.log('调用 AI 进行评审...');
const review = await getAIReview(prompt);
console.log('AI 评审完成');
console.log('评审结果:', review);
expect(typeof review).toBe('string');
expect(review.length).toBeGreaterThan(0);
console.log('AI 评审测试完成');
}, 120000); // 设置测试超时时间为 120 秒
});
// 测试添加评审评论
describe('addReviewComment', () => {
// it('should add review comment to merge request', async () => {
// const review = 'Test review comment';
// console.log('添加合并请求评论:', review);
// await expect(addReviewComment(
// process.env.CI_PROJECT_ID,
// 'merge_request',
// process.env.CI_MERGE_REQUEST_IID,
// review
// )).resolves.not.toThrow();
// }, 30000);
it('should add review comment to commit', async () => {
const review = 'Test review comment';
console.log('添加提交评论:', review);
await expect(addReviewComment(
process.env.CI_PROJECT_ID,
'push',
process.env.CI_COMMIT_SHA,
review
)).resolves.not.toThrow();
}, 30000);
});
// 测试完整评审流程
describe('processReview', () => {
// it('should process review for merge request', async () => {
// console.log('开始处理合并请求评审...');
// await expect(processReview(
// process.env.CI_PROJECT_ID,
// 'merge_request',
// process.env.CI_MERGE_REQUEST_IID
// )).resolves.not.toThrow();
// console.log('合并请求评审完成');
// }, 120000); // 设置超时时间为 120 秒
it('should process review for push', async () => {
console.log('开始处理推送评审...');
await expect(processReview(
process.env.CI_PROJECT_ID,
'push',
process.env.CI_COMMIT_SHA
)).resolves.not.toThrow();
console.log('推送评审完成');
}, 120000);
});
});

查看文件

@@ -1,25 +0,0 @@
// 设置测试环境变量
process.env.NODE_ENV = 'test';
// GitLab 相关配置
process.env.GITLAB_TOKEN = process.env.TEST_GITLAB_TOKEN;
process.env.GITLAB_URL = process.env.TEST_GITLAB_URL || 'http://git.dcloud.io';
// DeepSeek API 配置
process.env.DEEPSEEK_API_KEY = process.env.TEST_DEEPSEEK_API_KEY;
// 测试项目配置
process.env.TEST_PROJECT_ID = '602';
process.env.TEST_MERGE_REQUEST_IID = '1';
process.env.TEST_COMMIT_SHA = 'f07f0dfe33e4099256bb23412d004502973c55c8';
process.env.TEST_BRANCH = 'dev';
process.env.TEST_PIPELINE_SOURCE = 'merge_request_event';
process.env.CI_PROJECT_DIR = process.cwd();
// 添加新的配置
process.env.CI_PROJECT_ID = process.env.TEST_PROJECT_ID;
process.env.CI_MERGE_REQUEST_IID = process.env.TEST_MERGE_REQUEST_IID;
process.env.CI_COMMIT_SHA = process.env.TEST_COMMIT_SHA;
process.env.CI_COMMIT_BRANCH = process.env.TEST_BRANCH || 'dev';
process.env.CI_PIPELINE_SOURCE = process.env.TEST_PIPELINE_SOURCE || 'push';
process.env.CI_PROJECT_DIR = process.cwd() + '/../';

查看文件

@@ -1,81 +0,0 @@
# uni-app x 示例项目代码评审配置文件
# 项目基本信息
project:
reviewGuidelines: |
**请扮演一位经验丰富的 uni-app x 高级开发者,专注于对 Git Commit 进行代码评审。**
我将提供本次提交的以下信息:
1. 提交日志(Commit Message)
2. 代码变更内容(Diff)
3. 变更涉及到的代码上下文(包含注释)
我的项目使用 **uni-app x 框架** 实现示例功能,使用 **jest 框架** 进行断言和截图测试。
---
**请基于这些信息,针对 本次提交引入的变更 进行代码评审。你的评审应聚焦于:**
### 1. 变更目的与实际代码一致性
* 代码变更是否准确、完整地实现了提交日志中描述的目的或修复的问题?
* 提交日志是否清晰地解释了本次变更的意图?
### 2. 代码质量 (针对变更部分)
* **可读性与清晰度:**
* 新添加或修改的代码是否容易理解?
* 变量、函数、类、方法的命名是否符合规范且表达清晰?
* 代码结构是否合理?
* **潜在问题:**
* 变更是否可能引入新的 bug 或意外行为?
* 是否可能引入性能瓶颈(例如,在循环中执行昂贵的数据库操作,或者未优化 Eloquent 查询)?
* 是否存在潜在的资源泄漏或错误处理不当?
* **代码规范与最佳实践:**
* 新代码是否遵循 UTS 编码标准和项目内部的编码规范?
* 是否考虑到多端适配性(如 iOS、Android、H5、小程序等)?
* 是否遵循了 uni-app x 的最佳实践(如组件化、状态管理等)?
* 是否遵循了 jest 的最佳实践?
* **安全性:**
* 变更是否引入了新的安全风险?(特别关注用户输入处理、数据库交互、跨站脚本 XSS、跨站请求伪造 CSRF 等)
* 对于敏感操作,是否确保了适当的授权和认证?
* 涉及到手机号验证、邮箱验证、实人认证、三要素认证等付费验证时,需要增加合理的防刷机制。
### 3. 代码注释
* 新添加的注释是否清晰、准确、简洁,能够有效解释代码意图或复杂部分?
* 变更是否导致现有注释过时、不准确或产生误导?
* 是否有需要添加但缺失的关键注释(例如,对公共方法或复杂逻辑的解释)?
### 4. 整体变更结构
* 这次提交的变更范围是否合理(即,只包含了与提交目的相关的修改,没有混入其他不相关的改动)?
* 如果变更跨越多个文件,它们之间的逻辑联系是否清晰?
### 5. 测试文件变更
* 如果修改的是 .test.js 文件,需要验证测试用例是否合理、覆盖了相关功能,并且符合 jest 的最佳实践。
* 如果测试中涉及到截图测试(program.screenshot) 且没有在 app webview 模式(通过 process.env.UNI_AUTOMATOR_APP_WEBVIEW == 'true' 判断)时跳过,则需要设置为截长图模式,即 program.screenshot 配置参数 fullPage: true。
---
**请以清晰的格式(如列表或分节)输出评审结果,并:**
* **引用** 提交日志来作为评审的起点。
* **具体指出** 发现问题的代码位置(尽可能引用文件路径和相关的 diff 行号)。
* **详细描述** 发现的问题是什么。
* **提供具体、可操作的改进建议**。
* **标注问题的建议严重程度**(例如:`严重`, `主要`, `次要`, `建议`)。
* 如果基于本次变更看,代码质量良好且没有发现明显问题,也请简要说明。
* 将总结内容放在最前面输出,总结内容需要包含"总体评分(优、良、中、差)"、"推荐级别(可上线、建议修改、必须修改)"、"总体评价",方便评审人员快速评估。
* 回复时使用中文。
---
**以下是本次 Git Commit 的相关信息:**
# 忽略文件
ignore:
- "public/build/**"
- "node_modules/**"
- "*.lock"
- "code-review/**"
- ".env"
- ".env.*"
- "package-lock.json"
- ".gitlab-ci.yml"

查看文件

@@ -1,587 +0,0 @@
const { Gitlab } = require('@gitbeaker/rest');
const axios = require('axios');
const yaml = require('yaml');
const fs = require('fs');
const minimatch = require('minimatch');
const { execSync } = require('child_process');
require('dotenv').config();
// 阿里百炼 https://bailian.console.aliyun.com/
const BAILIAN_API_KEY = process.env.BAILIAN_API_KEY;
const BAILIAN_API_URL = 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions';
// 阿波罗AI https://api.ablai.top/personal
const ABLAI_API_KEY = process.env.ABLAI_API_KEY;
const ABLAI_API_URL = 'https://api.ablai.top/v1/chat/completions';
// Gemini CLI 配置
const CODE_ASSIST_ENDPOINT = process.env.CODE_ASSIST_ENDPOINT || 'https://ww3.html5plus.org/gemini';
const GOOGLE_CLOUD_ACCESS_TOKEN = process.env.GOOGLE_CLOUD_ACCESS_TOKEN;
const GOOGLE_GENAI_USE_GCA = process.env.GOOGLE_GENAI_USE_GCA || 'true';
const GEMINI_MODEL = process.env.GEMINI_MODEL || 'gemini-2.5-pro';
const GITLAB_TOKEN = process.env.GITLAB_TOKEN;
const GITLAB_URL = process.env.CI_SERVER_URL || 'http://git.dcloud.io';
const api = new Gitlab({
token: GITLAB_TOKEN,
host: GITLAB_URL
});
// AI 服务商配置
const AI_PROVIDERS = {
bailian: {
name: '阿里百炼',
apiKey: BAILIAN_API_KEY,
apiUrl: BAILIAN_API_URL,
envKey: 'BAILIAN_API_KEY'
},
ablai: {
name: '阿波罗',
apiKey: ABLAI_API_KEY,
apiUrl: ABLAI_API_URL,
envKey: 'ABLAI_API_KEY'
},
gemini_cli: {
name: 'Gemini CLI',
type: 'cli',
accessToken: GOOGLE_CLOUD_ACCESS_TOKEN,
endpoint: CODE_ASSIST_ENDPOINT,
useGCA: GOOGLE_GENAI_USE_GCA,
model: GEMINI_MODEL,
envKey: 'GOOGLE_CLOUD_ACCESS_TOKEN'
}
};
// 检查提交是否已经被评审过
async function isCommitReviewed(projectId, commitId) {
try {
const discussions = await api.CommitDiscussions.all(projectId, commitId);
return discussions.some(discussion =>
discussion.notes.some(note =>
note.body.includes('🤖 AI 代码评审结果')
)
);
} catch (error) {
console.error(`检查提交 ${commitId} 评审状态时出错:`, error);
return false;
}
}
// 加载项目配置
function loadProjectConfig() {
try {
// 在 GitLab CI 环境中,工作目录是 /builds/username/project-name/
const configPath = `${process.env.CI_PROJECT_DIR}/code-review/configs/code-review.yaml`;
const configContent = fs.readFileSync(configPath, 'utf8');
const config = yaml.parse(configContent);
if (!config || !config.project) {
throw new Error('配置文件格式错误');
}
return {
reviewGuidelines: config.project.reviewGuidelines || '',
ignoreFiles: config.ignore || [],
aiModel: config.project.aiModel || process.env.AI_MODEL || "gemini-2.5-pro",
provider: config.project.provider || process.env.AI_PROVIDER || 'ablai',
maxTokens: config.project.maxTokens || parseInt(process.env.AI_MAX_TOKENS) || 5000
};
} catch (error) {
console.error('Error loading config:', error);
return null;
}
}
// 生成 AI 评审提示词
function generateReviewPrompt(projectConfig, changes, commitInfo = null) {
const { reviewGuidelines } = projectConfig;
// 格式化变更信息
const formattedChanges = changes.map(change => {
return `
#### 文件路径:${change.file}
##### 变更内容:
${change.diff}
${change.content ? `##### 文件完整内容:
${change.content}` : ''}
`;
}).join('\n');
// 添加 commit 信息
const commitInfoText = commitInfo ? `${commitInfo.message}` : '';
return `
${reviewGuidelines}
### 提交日志 (Commit Message):
${commitInfoText}
### 代码变更及上下文:
${formattedChanges}
`;
}
// 添加重试函数 - 支持更多错误类型
async function retryWithDelay(fn, maxRetries = 5, delay = 3000) {
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
lastError = error;
// 判断是否应该重试
const shouldRetry =
// 5xx 服务器错误
(error.response && error.response.status >= 500) ||
// 网络错误
error.code === 'ECONNRESET' ||
error.code === 'ETIMEDOUT' ||
error.code === 'ENOTFOUND' ||
error.code === 'ECONNREFUSED' ||
// Axios 超时
error.code === 'ECONNABORTED' ||
// 429 限流错误
(error.response && error.response.status === 429);
if (shouldRetry && i < maxRetries - 1) {
const errorMsg = error.response
? `状态码: ${error.response.status}`
: `错误: ${error.code || error.message}`;
console.warn(`AI API 请求失败 (${errorMsg}),正在进行第 ${i + 1}/${maxRetries} 次重试...`);
// 如果是 429 限流,等待更长时间
const retryDelay = error.response?.status === 429 ? delay * 2 : delay;
await new Promise(resolve => setTimeout(resolve, retryDelay));
continue;
}
// 不应该重试的错误,直接抛出
throw error;
}
}
throw lastError;
}
// 使用 Gemini CLI 进行评审 - 带重试机制
async function getGeminiCLIReview(prompt, providerConfig, retryCount = 0) {
const MAX_RETRIES = 3;
const RETRY_DELAY = 3000; // 3秒
console.log('使用 Gemini CLI 调用...');
if (!providerConfig.accessToken) {
throw new Error(`Gemini Access Token (${providerConfig.envKey}) 未设置`);
}
if (!providerConfig.endpoint) {
throw new Error('CODE_ASSIST_ENDPOINT 未设置');
}
// 将 prompt 写入临时文件,避免命令行参数过长
const tempFile = `/tmp/gemini_prompt_${Date.now()}.txt`;
fs.writeFileSync(tempFile, prompt, 'utf8');
try {
// 构建环境变量
const env = {
...process.env,
CODE_ASSIST_ENDPOINT: providerConfig.endpoint,
GOOGLE_CLOUD_ACCESS_TOKEN: providerConfig.accessToken,
GOOGLE_GENAI_USE_GCA: providerConfig.useGCA
};
// 使用本地 gemini CLI 执行,从文件读取 prompt
const geminiPath = './node_modules/.bin/gemini';
const command = `${geminiPath} -m ${providerConfig.model} < "${tempFile}"`;
console.log(`执行命令: ${geminiPath} -m ${providerConfig.model} < [prompt file]`);
const result = execSync(command, {
env,
encoding: 'utf8',
maxBuffer: 50 * 1024 * 1024, // 50MB buffer
timeout: 600000, // 10 分钟超时
shell: true
});
// 过滤掉 Gemini CLI 的提示信息
const cleanedResult = result
.replace(/Data collection is disabled\./g, '')
.trim();
return cleanedResult;
} catch (error) {
// 判断是否应该重试
const shouldRetry =
// 命令执行超时
error.code === 'ETIMEDOUT' ||
// 命令执行失败(非零退出码),可能是临时网络问题
(error.status && error.status !== 0 && retryCount < MAX_RETRIES);
if (shouldRetry) {
console.warn(`Gemini CLI 调用失败 (${error.message}),正在进行第 ${retryCount + 1}/${MAX_RETRIES} 次重试...`);
// 清理临时文件
if (fs.existsSync(tempFile)) {
fs.unlinkSync(tempFile);
}
// 等待后重试
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY));
return getGeminiCLIReview(prompt, providerConfig, retryCount + 1);
}
// 不应该重试或已达到最大重试次数,抛出错误
throw error;
} finally {
// 清理临时文件
if (fs.existsSync(tempFile)) {
fs.unlinkSync(tempFile);
}
}
}
// 调用 AI API 进行评审
async function getAIReview(prompt, projectConfig) {
try {
console.log('调用 AI...');
console.log(prompt);
const model = projectConfig.aiModel || "gemini-3-pro-preview";
const provider = projectConfig.provider || 'ablai';
console.log('provider', provider);
// 获取服务商配置
const providerConfig = AI_PROVIDERS[provider];
if (!providerConfig) {
throw new Error(`不支持的服务商: ${provider}`);
}
// 根据服务商类型选择调用方式
if (providerConfig.type === 'cli') {
// 使用 Gemini CLI 调用
return await getGeminiCLIReview(prompt, providerConfig);
}
// 使用 HTTP API 调用
if (!providerConfig.apiKey) {
throw new Error(`${providerConfig.name} API Key (${providerConfig.envKey}) 未设置`);
}
// 创建 axios 实例
const axiosInstance = axios.create({
proxy: false,
timeout: 600000 // 设置超时时间为 10 分钟
});
// 使用重试机制发送请求
const response = await retryWithDelay(async () => {
return await axiosInstance.post(providerConfig.apiUrl, {
model: model,
messages: [{ role: "user", content: prompt }],
temperature: 0.7,
max_tokens: projectConfig.maxTokens || 5000
}, {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${providerConfig.apiKey}`
}
});
});
return response.data.choices[0].message.content;
} catch (error) {
console.error('Error calling AI:', error);
if (error.code === 'ECONNABORTED') {
console.error('API 请求超时,请检查网络连接或增加超时时间');
}
throw error;
}
}
// 获取代码变更内容
async function getChanges(projectId, sourceType, sourceId) {
try {
let changes;
if (sourceType === 'merge_request') {
console.log(`获取合并请求 ${sourceId} 的代码变更...`);
changes = await api.MergeRequests.allDiffs(projectId, sourceId, {
accessRawDiffs: true
});
console.log(`成功获取合并请求 ${sourceId} 的代码变更,共 ${changes.length} 个文件`);
} else if (sourceType === 'push') {
console.log(`获取提交 ${sourceId} 的代码变更...`);
// 获取单个 commit 的变更
const diff = await api.Commits.showDiff(projectId, sourceId);
changes = diff.map(change => ({
new_path: change.new_path,
old_path: change.old_path,
diff: change.diff
}));
console.log(`成功获取提交 ${sourceId} 的代码变更,共 ${changes.length} 个文件`);
} else {
console.error(`不支持的类型: ${sourceType}`);
throw new Error(`不支持的类型: ${sourceType}`);
}
const projectConfig = loadProjectConfig();
const ignorePatterns = projectConfig.ignoreFiles || [];
// 获取变更文件的完整内容
const changesWithContent = await Promise.all(changes
.filter(change => {
// 检查文件是否在忽略列表中
return !ignorePatterns.some(pattern => {
// 使用 minimatch 进行 glob 模式匹配
const shouldIgnore =
(change.new_path && minimatch(change.new_path, pattern)) ||
(change.old_path && minimatch(change.old_path, pattern));
if (shouldIgnore) {
console.log(`忽略文件: ${change.new_path || change.old_path} (匹配模式: ${pattern})`);
}
return shouldIgnore;
});
})
.map(async change => {
const filePath = change.new_path || change.old_path;
try {
console.log(`正在获取文件 ${filePath} 的完整内容...`);
// 获取文件的完整内容
const fileContent = await api.RepositoryFiles.show(projectId, filePath, sourceId);
// 对 base64 编码的内容进行解码
const decodedContent = Buffer.from(fileContent.content, 'base64').toString('utf-8');
console.log(`成功获取文件 ${filePath} 的完整内容`);
return {
file: filePath,
diff: change.diff,
content: decodedContent
};
} catch (error) {
console.error(`无法获取文件 ${filePath} 的完整内容:`, error);
return {
file: filePath,
diff: change.diff
};
}
}));
console.log(`成功处理所有文件变更,共 ${changesWithContent.length} 个文件`);
return changesWithContent;
} catch (error) {
console.error('获取代码变更失败:', error);
throw error;
}
}
// 添加评审评论
async function addReviewComment(projectId, sourceType, sourceId, review, retryCount = 0) {
const MAX_RETRIES = 3;
const RETRY_DELAY = 1000; // 1秒
try {
console.log(`添加评审评论 - 项目ID: ${projectId}, 来源类型: ${sourceType}, 来源ID: ${sourceId}`);
if (!projectId) {
throw new Error('项目ID不能为空');
}
if (!sourceId) {
throw new Error('来源ID不能为空');
}
if (!review) {
throw new Error('评审内容不能为空');
}
const note = `🤖 AI 代码评审结果:\n\n${review}`;
if (sourceType === 'merge_request') {
console.log('正在为合并请求添加评论...');
await api.MergeRequestNotes.create(projectId, sourceId, note);
console.log('合并请求评论添加成功');
} else if (sourceType === 'push') {
console.log('正在为提交添加评论...');
await api.CommitDiscussions.create(projectId, sourceId, note);
console.log('提交评论添加成功');
} else {
throw new Error(`不支持的来源类型: ${sourceType}`);
}
} catch (error) {
// 检查是否是连接错误(Keep-Alive 超时导致的 socket 关闭)
const isSocketError = error.cause?.code === 'UND_ERR_SOCKET' ||
error.message === 'fetch failed' ||
error.cause?.message?.includes('other side closed');
if (isSocketError && retryCount < MAX_RETRIES) {
console.warn(`连接错误,正在进行第 ${retryCount + 1}/${MAX_RETRIES} 次重试...`);
// 等待一段时间后重试
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY));
return addReviewComment(projectId, sourceType, sourceId, review, retryCount + 1);
}
console.error('添加评审评论失败:', {
error: error.message,
projectId,
sourceType,
sourceId,
reviewLength: review?.length,
retryCount
});
if (error.cause?.description) {
console.error('错误详情:', error.cause.description);
}
throw error;
}
}
// 主处理函数
async function processReview(projectId, sourceType, sourceId) {
try {
const projectConfig = loadProjectConfig();
if (!projectConfig) {
console.error('Project configuration not found');
process.exit(1);
}
if (sourceType === 'push') {
console.log('上一次提交的 SHA:', process.env.CI_COMMIT_BEFORE_SHA);
console.log('当前提交的 SHA:', process.env.CI_COMMIT_SHA);
console.log('当前分支:', process.env.CI_COMMIT_BRANCH);
// 获取本次 push 的所有 commit
let commits;
// 检查 CI_COMMIT_BEFORE_SHA 是否为全 0 (新分支的首次提交)
const isNewBranch = !process.env.CI_COMMIT_BEFORE_SHA ||
process.env.CI_COMMIT_BEFORE_SHA === '0000000000000000000000000000000000000000' ||
/^0+$/.test(process.env.CI_COMMIT_BEFORE_SHA);
if (process.env.CI_COMMIT_BEFORE_SHA && process.env.CI_COMMIT_SHA && !isNewBranch) {
// 正常的 push,比较两个 commit 之间的差异
console.log('正常 push,比较提交差异...');
commits = await api.Repositories.compare(projectId, process.env.CI_COMMIT_BEFORE_SHA, process.env.CI_COMMIT_SHA);
commits = commits.commits || [];
console.log('获取本次提交的信息:', commits);
} else {
// 新分支的首次提交或没有 BEFORE_SHA,只获取当前提交
console.log('新分支首次提交或无法比较,获取当前提交...');
commits = await api.Commits.all(projectId, {
ref_name: process.env.CI_COMMIT_BRANCH,
per_page: 1
});
console.log('获取首次提交的信息:', commits);
}
// 过滤掉合并分支的提交
commits = commits.filter(commit => !commit.message.startsWith('Merge branch'));
console.log(`获取到 ${commits.length} 个提交需要评审(已过滤合并分支的提交)`);
// 对每个 commit 进行评审
for (const commit of commits) {
console.log(`开始评审提交: ${commit.id}`);
console.log(`提交信息: ${commit.message}`);
// 检查提交是否已经被评审过
const isReviewed = await isCommitReviewed(projectId, commit.id);
if (isReviewed) {
console.log(`提交 ${commit.id} 已经评审过,跳过评审`);
continue;
}
// 获取该 commit 的变更
const changes = await getChanges(projectId, sourceType, commit.id);
if (changes.length === 0) {
console.log(`提交 ${commit.id} 没有代码变更,跳过评审`);
continue;
}
console.log(`提交 ${commit.id} 包含 ${changes.length} 个文件变更`);
// 生成评审提示词
const prompt = generateReviewPrompt(projectConfig, changes, {
author_name: commit.author_name,
created_at: commit.created_at,
message: commit.message,
ref_name: process.env.CI_COMMIT_BRANCH
});
// 获取 AI 评审结果
const review = await getAIReview(prompt, projectConfig);
console.log(`AI 评审结果: ${review}`);
// 添加评审评论到 commit
await addReviewComment(projectId, sourceType, commit.id, review);
console.log(`提交 ${commit.id} 评审完成`);
}
} else if (sourceType === 'merge_request') {
const changes = await getChanges(projectId, sourceType, sourceId);
if (changes.length === 0) {
console.log('No changes to review');
return;
}
// 获取合并请求信息
const mrInfo = await api.MergeRequests.show(projectId, sourceId);
const prompt = generateReviewPrompt(projectConfig, changes, {
author_name: mrInfo.author.name,
created_at: mrInfo.created_at,
message: mrInfo.description,
ref_name: mrInfo.source_branch
});
const review = await getAIReview(prompt, projectConfig);
await addReviewComment(projectId, sourceType, sourceId, review);
}
console.log('Review completed successfully');
} catch (error) {
console.error('Error processing review:', error);
if (error.cause?.description?.includes('401 Unauthorized')) {
console.error('GitLab API authentication failed. Please check your GITLAB_TOKEN.');
}
// 在测试环境中抛出错误,而不是退出进程
if (process.env.NODE_ENV === 'test') {
throw error;
}
process.exit(1);
}
}
// 导出需要测试的函数
module.exports = {
loadProjectConfig,
generateReviewPrompt,
getAIReview,
getGeminiCLIReview,
getChanges,
addReviewComment,
processReview
};
// 只在直接运行 index.js 时执行
if (require.main === module) {
const projectId = process.env.CI_PROJECT_ID;
const sourceType = process.env.CI_PIPELINE_SOURCE === 'merge_request_event' ? 'merge_request' : 'push';
const sourceId = sourceType === 'merge_request' ? process.env.CI_MERGE_REQUEST_IID : process.env.CI_COMMIT_SHA;
if (!GITLAB_TOKEN) {
console.error('GITLAB_TOKEN is not set');
process.exit(1);
}
if (!projectId) {
console.error('CI_PROJECT_ID is not set');
process.exit(1);
}
if (!sourceId) {
console.error('Source ID is not set');
process.exit(1);
}
processReview(projectId, sourceType, sourceId);
}

文件差异内容过多而无法显示 加载差异

查看文件

@@ -1,36 +0,0 @@
{
"name": "uni-ai-code-review",
"version": "1.0.0",
"description": "AI 代码评审系统",
"main": "index.js",
"scripts": {
"test": "jest",
"start": "node index.js"
},
"dependencies": {
"@gitbeaker/rest": "^42.5.0",
"@google/gemini-cli": "^0.19.1",
"axios": "^1.6.0",
"dotenv": "^16.3.1",
"minimatch": "^5.1.6",
"yaml": "^2.3.4"
},
"devDependencies": {
"jest": "^29.7.0"
},
"jest": {
"testEnvironment": "node",
"coverageDirectory": "./coverage",
"collectCoverageFrom": [
"**/*.js",
"!**/*.test.js",
"!__tests__/setup.js"
],
"setupFiles": [
"./__tests__/setup.js"
],
"testMatch": [
"**/__tests__/**/*.test.js"
]
}
}

查看文件

@@ -6,6 +6,7 @@
import {
AiChatReply,
AiChatRequest,
CardContact,
CardProfile,
CardPortalDetail,
LeadEntity,
@@ -15,6 +16,7 @@ import {
NfcUidParams,
OwnerPageData,
OwnerPageQuery,
QuotaCheckResult,
RadarTrack,
SubmitLeadParams,
RecordRadarTrackParams,
@@ -160,5 +162,52 @@ export function fetchCardQrcodeSvg(
});
}
export function fetchHallContactPage(
query: OwnerPageQuery
): Promise<OwnerPageData<CardContact>> {
return httpPost<OwnerPageData<CardContact>>('/admin/card/contact/hallPage', query);
}
export function collectHallContact(
peerCardId: string | number,
ownerCardId?: string | number
): Promise<CardContact> {
const payload: Record<string, string | number> = { peerCardId };
if (ownerCardId != null) {
payload.ownerCardId = ownerCardId;
}
return httpPost<CardContact>('/admin/card/contact/collect', payload);
}
export function fetchTeamWallPage(
query: OwnerPageQuery
): Promise<OwnerPageData<CardContact>> {
return httpPost<OwnerPageData<CardContact>>('/admin/card/contact/teamWall', query);
}
export function fetchCardQuota(
tenantId?: string | number
): Promise<QuotaCheckResult> {
const payload: Record<string, string | number> = { resourceType: 'cards' };
if (tenantId != null && String(tenantId) !== '') {
payload.tenantId = tenantId;
}
return httpPost<QuotaCheckResult>('/admin/team/billing/quota', payload);
}
export function confirmHallExchange(
exchangeCode: string,
actingCardId?: string | number
): Promise<{ status: string; isDualSynced: boolean }> {
const payload: Record<string, string | number> = { exchangeCode };
if (actingCardId != null) {
payload.actingCardId = actingCardId;
}
return httpPost<{ status: string; isDualSynced: boolean }>(
'/admin/card/exchange/confirm',
payload
);
}
export * from './types.uts';
export * from './request.uts';

查看文件

@@ -104,7 +104,7 @@ export function parseDeepLink(url?: string): DeepLinkRouteInfo {
*/
export function buildAppPageRoute(info: DeepLinkRouteInfo): string {
if (!info || !info.isValid) {
return '/pages/owner/home';
return '/pages/owner/hall/index';
}
const queryParts: string[] = [];
@@ -116,5 +116,5 @@ export function buildAppPageRoute(info: DeepLinkRouteInfo): string {
if (info.source) queryParts.push(`source=${info.source}`);
const queryString = queryParts.length > 0 ? `?${queryParts.join('&')}` : '';
return `/pages/card/portal${queryString}`;
return `/pages/portal/index${queryString}`;
}

查看文件

@@ -105,7 +105,7 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
'radar.visitor_trace': '访问轨迹',
'radar.lead_score': '意向度评分',
'crm.contacts': '联系人列表',
'crm.leads': '商机线索',
'crm.leads': '客户管理',
'crm.follow_up': '跟进记录',
'portal.unavailable': '微官网暂不可用',
'portal.retry': '重新加载',
@@ -161,7 +161,7 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
'radar.visitor_trace': 'Visitor Trail',
'radar.lead_score': 'Lead Score',
'crm.contacts': 'Contacts',
'crm.leads': 'Leads',
'crm.leads': 'Customers',
'crm.follow_up': 'Follow-up',
'portal.unavailable': 'This page is unavailable',
'portal.retry': 'Reload',
@@ -217,7 +217,7 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
'radar.visitor_trace': 'مسار الزائر',
'radar.lead_score': 'درجة العميل المحتمل',
'crm.contacts': 'جهات الاتصال',
'crm.leads': 'العملاء المحتملون',
'crm.leads': 'العملاء',
'crm.follow_up': 'المتابعة',
'portal.unavailable': 'الصفحة غير متاحة حاليا',
'portal.retry': 'إعادة التحميل',
@@ -273,7 +273,7 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
'radar.visitor_trace': 'Recorrido del visitante',
'radar.lead_score': 'Puntuación de interés',
'crm.contacts': 'Contactos',
'crm.leads': 'Oportunidades',
'crm.leads': 'Clientes',
'crm.follow_up': 'Seguimiento',
'portal.unavailable': 'Esta página no está disponible',
'portal.retry': 'Recargar',
@@ -329,7 +329,7 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
'radar.visitor_trace': 'Besucherpfad',
'radar.lead_score': 'Lead-Score',
'crm.contacts': 'Kontakte',
'crm.leads': 'Leads',
'crm.leads': 'Kunden',
'crm.follow_up': 'Nachverfolgung',
'portal.unavailable': 'Diese Seite ist nicht verfügbar',
'portal.retry': 'Neu laden',
@@ -385,7 +385,7 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
'radar.visitor_trace': 'Parcours visiteur',
'radar.lead_score': 'Score d’intérêt',
'crm.contacts': 'Contacts',
'crm.leads': 'Prospects',
'crm.leads': 'Clients',
'crm.follow_up': 'Suivi',
'portal.unavailable': 'Cette page est indisponible',
'portal.retry': 'Recharger',
@@ -441,7 +441,7 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
'radar.visitor_trace': '訪問履歴',
'radar.lead_score': '関心度スコア',
'crm.contacts': '連絡先',
'crm.leads': 'リード',
'crm.leads': '顧客',
'crm.follow_up': 'フォロー記録',
'portal.unavailable': 'このページは現在ご利用いただけません',
'portal.retry': '再読み込み',

查看文件

@@ -10,13 +10,26 @@ let currentBaseUrl: string = 'http://127.0.0.1:9000';
if (typeof window !== 'undefined' && window.location != null) {
const protocol = window.location.protocol != null && window.location.protocol !== '' ? window.location.protocol : 'http:';
const hostname = window.location.hostname != null && window.location.hostname !== '' ? window.location.hostname : '127.0.0.1';
currentBaseUrl = `${protocol}//${hostname}:9000`;
const port = window.location.port;
if (port === '5173' || port === '5174' || port === '3000' || port === '8080') {
currentBaseUrl = `${protocol}//${hostname}:9000`;
} else if (port != null && port !== '' && port !== '80' && port !== '443') {
currentBaseUrl = `${protocol}//${hostname}:${port}`;
} else {
currentBaseUrl = `${protocol}//${hostname}`;
}
}
// #endif
let currentTimeout: number = 10000;
export function getBaseUrl(): string {
try {
const override = uni.getStorageSync('api_base_url');
if (override != null && String(override).trim() !== '') {
return String(override).trim();
}
} catch (e) {}
return currentBaseUrl;
}
@@ -35,22 +48,41 @@ export function setRequestTimeout(timeout: number): void {
export function getAuthToken(): string {
try {
const val = uni.getStorageSync('token');
return val != null ? String(val) : '';
} catch (e) {
return '';
}
if (val != null && String(val) !== '') {
return String(val);
}
} catch (e) {}
try {
if (typeof window !== 'undefined' && typeof localStorage !== 'undefined') {
const local = localStorage.getItem('token');
if (local != null && String(local) !== '') {
return String(local);
}
}
} catch (e) {}
return '';
}
export function setAuthToken(token: string): void {
try {
uni.setStorageSync('token', token);
} catch (e) {}
try {
if (typeof window !== 'undefined' && typeof localStorage !== 'undefined') {
localStorage.setItem('token', token);
}
} catch (e) {}
}
export function clearAuthToken(): void {
try {
uni.removeStorageSync('token');
} catch (e) {}
try {
if (typeof window !== 'undefined' && typeof localStorage !== 'undefined') {
localStorage.removeItem('token');
}
} catch (e) {}
}
export function getTenantId(): string {

查看文件

@@ -85,7 +85,23 @@ export interface CardPortalDetail {
export type LeadStatus = 'new' | 'contacted' | 'hot' | 'won' | 'archived';
export type LeadSource = 'nfc' | 'qr' | 'link' | 'ai_chat';
export type LeadSource = 'nfc' | 'qr' | 'link' | 'ai_chat' | 'exchange';
export type CardContactSource = 'exchange' | 'collect' | 'scan';
export interface CardContact {
id?: string | number;
ownerUserId: number;
ownerCardId?: number | null;
peerCardId: number;
source: CardContactSource;
leadId?: number | null;
peerName?: string;
peerTitle?: string;
peerCompany?: string;
peerAvatarUrl?: string;
peerSlug?: string;
}
export interface LeadEntity {
id: string | number;
@@ -184,6 +200,16 @@ export interface OwnerPageQuery {
visitorId?: string;
}
export interface QuotaCheckResult {
used: number;
limit: number;
remaining: number;
percentage: number;
warning: boolean;
exceeded: boolean;
plan?: string;
}
export interface OwnerPageData<T> {
list: T[];
pagination: {

查看文件

@@ -1,375 +0,0 @@
/* 主题变量 */
.theme-light {
--list-background-color: #ffffff;
--background-color: #f8f8f8;
--active-color:#a0a0a0;
--active-background-color: #f8f8f8;
--text-color: #333333;
--arrow-color: #999999;
--border-color: rgba(0, 0, 0, .06);
}
.theme-dark {
--list-background-color: #2d2d2d;
--background-color: #1a1a1a;
--active-color:#8f8f8f;
--active-background-color: #3b3b3b;
--text-color: #ffffff;
--arrow-color: #ffffff;
--border-color: rgba(255, 255, 255, 0.1);
}
.uni-h1 {
font-size: 28px;
font-weight: bold;
border-bottom: 1px solid #ccc;
margin: 8px 0;
}
.uni-h2 {
font-size: 24px;
font-weight: bold;
border-bottom: 1px solid #ccc;
margin: 8px 0;
}
.uni-h3 {
font-size: 20px;
font-weight: bold;
margin: 8px 0;
}
.uni-h4 {
font-weight: bold;
margin: 8px 0;
}
.uni-theme-text{
color: var(--text-color);
}
.uni-padding-wrap {
padding: 0 15px;
}
.uni-title {
padding: 10px 0;
}
.uni-title-text {
font-size: 15px;
font-weight: bold;
color: var(--text-color, #333333);
}
.uni-subtitle-text {
color: #888;
font-size: 12px;
font-weight: bold;
margin-top: 5px;
}
.uni-info {
font-size: 12px;
color: #999;
}
.uni-common-mb {
margin-bottom: 15px;
}
.uni-common-pb {
padding-bottom: 15px;
}
.uni-common-pl {
padding-left: 15px;
}
.uni-common-mt {
margin-top: 15px;
}
.uni-hello-text {
color: var(--text-color, #333333);
line-height: 22px;
}
.uni-tips{
font-size: 13px;
color: #0958d9;
background-color: #e6f4ff;
border-left: 4px solid #91caff;
padding: 10px 12px;
border-radius: 4px;
margin: 10px;
}
.uni-list {
background-color: var(--list-background-color,#ffffff);
position: relative;
display: flex;
flex-direction: column;
border-bottom: 1px solid #c8c7cc;
}
.uni-list-cell {
position: relative;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.uni-list-cell-padding {
padding: 10px 15px;
}
.uni-list-cell-line {
border-bottom: 1px solid var(--border-color, rgba(0, 0, 0, .06));
}
.uni-list-cell-hover {
background-color: var(--active-background-color,#f8f8f8);
}
.uni-list-cell-pd {
padding: 11px 15px 11px 0;
}
.uni-list-cell-left {
padding: 0 15px 0 10px;
}
.uni-list-cell-db,
.uni-list-cell-right {
flex: 1;
}
.uni-list-cell-db-text {
width: 100%;
/* #ifdef WEB || MP */
word-break: break-all;
/* #endif */
}
.uni-label {
width: 105px;
}
.uni-input {
height: 25px;
padding: 8px 13px;
font-size: 14px;
flex: 1;
box-sizing: content-box;
background: var(--list-background-color,#ffffff);
}
/*
.uni-flex {
display: flex;
flex-direction: row;
}
*/
.uni-flex-item {
flex: 1;
}
.uni-row {
flex-direction: row;
}
.uni-column {
flex-direction: column;
}
.uni-bg-red {
background: #F76260;
}
.uni-bg-green {
background: #09BB07;
}
.uni-bg-blue {
background: #007AFF;
}
.uni-btn-v {
padding: 5px 0;
}
.uni-btn {
margin-top: 10px;
}
.uni-link {
color: #576B95;
font-size: 13px;
}
.uni-center {
flex-direction: row;
justify-content: center;
}
.uni-textarea {
padding: 9px;
line-height: 1.6;
font-size: 14px;
}
.uni-icon-size {
width: 14px;
height: 14px;
}
.uni-container {
padding: 15px;
background-color: var(--background-color,#f8f8f8);
}
.uni-header-logo {
padding: 15px 15px;
flex-direction: column;
justify-content: center;
align-items: center;
margin-top: 5px;
}
.uni-header-image {
width: 80px;
height: 80px;
}
.uni-text-box {
margin-bottom: 20px;
}
.hello-text {
color: var(--text-color, #333333);
font-size: 14px;
line-height: 20px;
}
.uni-panel {
margin-bottom: 12px;
}
.text-disabled {
color: var(--active-color)!important;
}
/* --tab-bar-begin-- */
.uni-panel-h {
background-color: var(--list-background-color);
flex-direction: row !important;
justify-content: space-between !important;
align-items: center !important;
padding: 12px;
}
.uni-panel-h-on {
background-color: #f0f0f0;
}
.uni-panel-text {
color: var(--text-color, #333333);
font-size: 14px;
font-weight: normal;
}
.uni-navigate-item {
flex-direction: row;
align-items: center;
background-color: var(--list-background-color);
border-top-style: solid;
border-top-color: var(--border-color);
border-top-width: 1px;
padding: 12px 16px;
justify-content: space-between;
/* #ifdef WEB */
/* cursor: pointer; */
/* #endif */
}
.is--active {
background-color: var(--active-background-color);
}
.uni-navigate-text {
color: var(--text-color, #333333);
font-size: 14px;
font-weight: normal;
width: 94%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* left-windows */
.left-win-active {
color: #007AFF !important;
}
/* --tab-bar-end-- */
/* #ifdef APP */
.page-scroll-view {
flex: 1;
}
/* #endif */
/* web端适配暗黑主题 */
/* #ifdef WEB */
@media (prefers-color-scheme: dark) {
/* 增加deep,穿透所有页面和组件 */
:deep(body),
:deep(uni-page-body) {
color: rgba(0, 0, 0, 0.9);
}
.uni-top-window .tab-bar-flex :deep(.uni-tabbar){
background-color: #2d2d2d!important;
}
/* 设置滚动条整体宽度和背景色 */
::-webkit-scrollbar {
width: 8px;
background: #464646;
}
/* 设置滚动条滑块颜色 */
::-webkit-scrollbar-thumb {
background: #aaaaaa;
border-radius: 4px;
}
/* 鼠标悬停在滑块上时的颜色 */
::-webkit-scrollbar-thumb:hover {
background: #818181;
}
/* 滚动条轨道颜色 */
::-webkit-scrollbar-track {
background: #464646;
border-radius: 4px;
}
}
/* #endif */
/* #ifdef MP */
page {
color: rgba(0, 0, 0, 0.9);
}
/* #endif */
.pl-4{
padding-left: 4px;
}
.pr-4{
padding-right: 4px;
}
.margin-10{
margin: 10px;
}

查看文件

@@ -1,9 +0,0 @@
import { state } from '@/store/index.uts'
export const isDarkMode = computed(() : boolean => {
return state.isDarkMode
})
export const darkModeClass = computed(() : string => {
return state.isDarkMode ? 'theme-dark' : 'theme-light'
})

查看文件

@@ -1,48 +0,0 @@
module.exports = {
"is-custom-runtime": false,
"UNI_TEST_CUSTOM_ENV": {
// 以下3个配置项用于定义以App-WebView方式运行的H5页面地址,方便自动化测试App-WebView场景
// "UNI_AUTOMATOR_APP_WEBVIEW": "true",
// "UNI_WEB_SERVICE_URL": "http://xxx.com/xxx.html",
// "UNI_AUTOMATOR_APP_WEBVIEW_SRC": "http://xxx.com/xxx.html"
},
"compile": true,
"h5": {
"options": {
"headless": true
},
"executablePath": ""
},
"mp-weixin": {
"port": 9420,
"account": "",
"args": "",
"cwd": "",
"launch": true,
"teardown": "disconnect",
"remote": false,
"executablePath": ""
},
"app-plus": {
"android": {
"id": "",
"executablePath": ""
},
"version": "",
"ios": {
"id": "",
"executablePath": ""
},
"uni-app-x": {
"version": "/Applications/HBuilderX-Dev.app/Contents/HBuilderX/plugins/uniappx-launcher/base/version.txt",
"android": {
"id": "emulator-5554",
"executablePath": "/Applications/HBuilderX-Dev.app/Contents/HBuilderX/plugins/uniappx-launcher/base/android_base.apk"
},
"ios": {
"id": "CA80343E-7D4C-401D-81BA-B5B7D446C11D",
"executablePath": "/Applications/HBuilderX-Dev.app/Contents/HBuilderX/plugins/uniappx-launcher/base/Pandora_simulator.app"
}
}
}
}

查看文件

@@ -1,16 +0,0 @@
const fs = require('fs')
const {
execSync
} = require('child_process')
const message = fs.readFileSync(process.argv[2]).toString('utf8').toLowerCase()
const branch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim()
if (
(branch === 'master' || branch === 'alpha') &&
!message.startsWith('merge') &&
!message.startsWith('*')
) {
console.log('You are not allowed to commit directly to master or alpha branch')
process.exit(1)
}

查看文件

@@ -1,10 +0,0 @@
{
"app": {
"bundleName": "io.dcloud.uniappx",
"vendor": "example",
"versionCode": 10000,
"versionName": "1.0.0",
"icon": "$media:layered_image",
"label": "$string:app_name"
}
}

查看文件

@@ -1,40 +0,0 @@
{
"string": [
{
"name": "app_name",
"value": "OmniCard"
},
{
"name": "permission_location_tips",
"value": "正在申请获取您的位置信息,以便为您提供相应的服务"
},
{
"name": "permission_write_imagevideo_tips",
"value": "正在申请写入相册权限,以便保存文件到相册"
},
{
"name": "permission_get_networkinfo_tips",
"value": "正在申请获取网络信息权限,以便为您提供相应的服务"
},
{
"name": "permission_microphone",
"value": "正在申请使用麦克风权限,以便为您提供相应的服务"
},
{
"name": "permission_read_pasteboard",
"value": "正在申请读取剪切板,以便为您提供相应的服务"
},
{
"name": "permission_read_privacywindow",
"value": "正在申请隐私窗口权限,以便为您提供相应的服务"
},
{
"name": "permission_read_accessbiometric",
"value": "正在申请访问设备的生物识别功能权限,以便为您提供相应的服务"
},
{
"name": "write_calendar_reason",
"value": "正在申请写入日历权限,以便为您提供相应的服务"
}
]
}

查看文件

@@ -1,83 +0,0 @@
{
"app": {
"signingConfigs": [
{
"name": "default",
"type": "HarmonyOS",
"material": {
"storePassword": "00000019350D4E7C95A55A5FBEF0F5AF0AC08D9C030BD36456ED60DA7143472A9AF617AD8A18F20233",
"certpath": "./config/hello-uni-app-x.cer",
"keyAlias": "debugKey",
"keyPassword": "0000001958D675423AB2CAB19457483AD81FFBEFD3615C8404D1FB75AE216154CD780B356AF3D0432C",
"profile": "./config/hello-uni-app-x.p7b",
"signAlg": "SHA256withECDSA",
"storeFile": "./config/hello-uni-app-x.p12"
}
},
{
"name": "release",
"type": "HarmonyOS",
"material": {
"storePassword": "00000019350D4E7C95A55A5FBEF0F5AF0AC08D9C030BD36456ED60DA7143472A9AF617AD8A18F20233",
"certpath": "./config/release/io.dcloud.uniappx.cer",
"keyAlias": "debugKey",
"keyPassword": "0000001958D675423AB2CAB19457483AD81FFBEFD3615C8404D1FB75AE216154CD780B356AF3D0432C",
"profile": "./config/release/io.dcloud.uniappx.p7b",
"signAlg": "SHA256withECDSA",
"storeFile": "./config/hello-uni-app-x.p12"
}
}
],
"products": [
{
"name": "default",
"signingConfig": "default",
"compatibleSdkVersion": "6.0.0(20)",
"compatibleSdkVersionStage": "beta6",
"runtimeOS": "HarmonyOS",
"buildOption": {
"strictMode": {
"caseSensitiveCheck": true,
"useNormalizedOHMUrl": true
}
}
},
{
"name": "release",
"signingConfig": "release",
"compatibleSdkVersion": "6.0.0(20)",
"compatibleSdkVersionStage": "beta6",
"runtimeOS": "HarmonyOS",
"buildOption": {
"strictMode": {
"caseSensitiveCheck": true,
"useNormalizedOHMUrl": true
}
}
}
],
"buildModeSet": [
{
"name": "debug"
},
{
"name": "release"
}
]
},
"modules": [
{
"name": "entry",
"srcPath": "./entry",
"targets": [
{
"name": "default",
"applyToProducts": [
"default",
"release"
]
}
]
}
]
}

查看文件

@@ -1,52 +0,0 @@
-----BEGIN CERTIFICATE-----
MIICGjCCAaGgAwIBAgIIShhpn519jNAwCgYIKoZIzj0EAwMwUzELMAkGA1UEBhMC
Q04xDzANBgNVBAoMBkh1YXdlaTETMBEGA1UECwwKSHVhd2VpIENCRzEeMBwGA1UE
AwwVSHVhd2VpIENCRyBSb290IENBIEcyMB4XDTIwMDMxNjAzMDQzOVoXDTQ5MDMx
NjAzMDQzOVowUzELMAkGA1UEBhMCQ04xDzANBgNVBAoMBkh1YXdlaTETMBEGA1UE
CwwKSHVhd2VpIENCRzEeMBwGA1UEAwwVSHVhd2VpIENCRyBSb290IENBIEcyMHYw
EAYHKoZIzj0CAQYFK4EEACIDYgAEWidkGnDSOw3/HE2y2GHl+fpWBIa5S+IlnNrs
GUvwC1I2QWvtqCHWmwFlFK95zKXiM8s9yV3VVXh7ivN8ZJO3SC5N1TCrvB2lpHMB
wcz4DA0kgHCMm/wDec6kOHx1xvCRo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T
AQH/BAUwAwEB/zAdBgNVHQ4EFgQUo45a9Vq8cYwqaiVyfkiS4pLcIAAwCgYIKoZI
zj0EAwMDZwAwZAIwMypeB7P0IbY7c6gpWcClhRznOJFj8uavrNu2PIoz9KIqr3jn
BlBHJs0myI7ntYpEAjBbm8eDMZY5zq5iMZUC6H7UzYSix4Uy1YlsLVV738PtKP9h
FTjgDHctXJlC5L7+ZDY=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDATCCAoigAwIBAgIIXmuDXbWpOB8wCgYIKoZIzj0EAwMwUzELMAkGA1UEBhMC
Q04xDzANBgNVBAoMBkh1YXdlaTETMBEGA1UECwwKSHVhd2VpIENCRzEeMBwGA1UE
AwwVSHVhd2VpIENCRyBSb290IENBIEcyMB4XDTIwMDcwOTAyMDQyNFoXDTMwMDcw
NzAyMDQyNFowYjELMAkGA1UEBgwCQ04xDzANBgNVBAoMBkh1YXdlaTETMBEGA1UE
CwwKSHVhd2VpIENCRzEtMCsGA1UEAwwkSHVhd2VpIENCRyBEZXZlbG9wZXIgUmVs
YXRpb25zIENBIEcyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE65LdoIZh1hlpZ2gP
bJ6gPhHsvYSRe22KETgdqeVeYnrbRHI9wsPT6RGYS+pU4mPl6wxzgDMqN6SY/BoZ
luhkE1PzaHoPoNIWIq0O33hpyKyyYwAacIUEjYurkw1E9r9no4IBGDCCARQwHwYD
VR0jBBgwFoAUo45a9Vq8cYwqaiVyfkiS4pLcIAAwHQYDVR0OBBYEFNtek7Ij6NDk
/nF6Zumkc0dbf/NeMEYGA1UdIAQ/MD0wOwYEVR0gADAzMDEGCCsGAQUFBwIBFiVo
dHRwOi8vY3BraS1jYXdlYi5odWF3ZWkuY29tL2Nwa2kvY3BzMBIGA1UdEwEB/wQI
MAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMGYGA1UdHwRfMF0wW6BZoFeGVWh0dHA6
Ly9jcGtpLWNhd2ViLmh1YXdlaS5jb20vY3BraS9zZXJ2bGV0L2NybEZpbGVEb3du
LmNybD9jZXJ0eXBlPTEwJi9yb290X2cyX2NybC5jcmwwCgYIKoZIzj0EAwMDZwAw
ZAIwWO1X5q2MdfpR1Q237GpUHGbL1C13rGyFg2p3AYo44FpZ2/A9ss0wOHKM4KDl
ZPqdAjBLkf8NPZy7KVog98+iCTLq35DJ2ZVxkCxknA9YhiHVyXf4HPm4JlT7rW7o
Q+FzM3c=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDFDCCApqgAwIBAgIOCf1Cn8+RhHsVbyIjBagwCgYIKoZIzj0EAwMwYjELMAkG
A1UEBgwCQ04xDzANBgNVBAoMBkh1YXdlaTETMBEGA1UECwwKSHVhd2VpIENCRzEt
MCsGA1UEAwwkSHVhd2VpIENCRyBEZXZlbG9wZXIgUmVsYXRpb25zIENBIEcyMB4X
DTI2MDcwMzA3NTkwNFoXDTI3MDcwMzA3NTkwNFowgcMxCzAJBgNVBAYTAkNOMTkw
NwYDVQQKDDDmlbDlrZflpKnloILvvIjljJfkuqzvvInnvZHnu5zmioDmnK/mnInp
mZDlhazlj7gxHDAaBgNVBAsMEzEyNTA2Njg4MTI5NjYwNTcxNTMxWzBZBgNVBAMM
UuaVsOWtl+WkqeWggu+8iOWMl+S6rO+8iee9kee7nOaKgOacr+aciemZkOWFrOWP
uCgxMjUwNjY4ODEyOTY2MDU3MTUzKVwsRGV2ZWxvcG1lbnQwWTATBgcqhkjOPQIB
BggqhkjOPQMBBwNCAAS7ko2FpS2JN9JbwUe6N765yWDIJ6vtt2fUIezicF6OFwmD
T9T3cVRU8fS08/KxKqDS6Z5c1syUIbSB8Q35zoWXo4HRMIHOMB0GA1UdDgQWBBQ2
KMbkY7ynzhJ0h3ZLuW1ezpUUAzAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFNte
k7Ij6NDk/nF6Zumkc0dbf/NeMFkGA1UdHwRSMFAwTqBMoEqGSGh0dHA6Ly9oNWhv
c3RpbmctZHJjbi5kYmFua2Nkbi5jbi9jY2g1L2NybC9oZHJjYWcyL0h1YXdlaUNC
R0hEUkcyY3JsLmNybDAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUH
AwMwCgYIKoZIzj0EAwMDaAAwZQIwKBU6ALP6lyRpMGXoRJ/FH54w3dUXieeVIisR
47e3XdoVikmLZvWYeVoQriEJpzzLAjEAi/RTFHuabaMn+zQqRRSi0Auljiza3X3Q
tNaOc7+VUqUCiRe1hv+QZcP82GzjUb28
-----END CERTIFICATE-----

查看文件

@@ -1,9 +0,0 @@
-----BEGIN NEW CERTIFICATE REQUEST-----
MIIBNjCB3AIBADBKMQkwBwYDVQQGEwAxCTAHBgNVBAgTADEJMAcGA1UEBxMAMQkw
BwYDVQQKEwAxCTAHBgNVBAsTADERMA8GA1UEAxMIZGVidWdLZXkwWTATBgcqhkjO
PQIBBggqhkjOPQMBBwNCAAS7ko2FpS2JN9JbwUe6N765yWDIJ6vtt2fUIezicF6O
FwmDT9T3cVRU8fS08/KxKqDS6Z5c1syUIbSB8Q35zoWXoDAwLgYJKoZIhvcNAQkO
MSEwHzAdBgNVHQ4EFgQUNijG5GO8p84SdId2S7ltXs6VFAMwCgYIKoZIzj0EAwID
SQAwRgIhALx/YYkNiOa30CNHyz8BrOyCuqe3PSTTNW9aqV+evleAAiEA10DAwzkI
G8dJ+/YG+AtlEsxbquQl2mugZcR8pxIqZeU=
-----END NEW CERTIFICATE REQUEST-----

查看文件

@@ -1 +0,0 @@
<EFBFBD> <0C><><EFBFBD>Zf/<2F><>[Z<>I

查看文件

@@ -1 +0,0 @@
<EFBFBD><EFBFBD><EFBFBD>a*E<>R<EFBFBD>=yܨ<16>{

查看文件

@@ -1 +0,0 @@
<05>GxtqB<71><42><1D><>O

查看文件

@@ -1 +0,0 @@
<EFBFBD>}<7D>|$<24><>-;Pk<50><6B><EFBFBD>o

查看文件

@@ -1,52 +0,0 @@
-----BEGIN CERTIFICATE-----
MIICGjCCAaGgAwIBAgIIShhpn519jNAwCgYIKoZIzj0EAwMwUzELMAkGA1UEBhMC
Q04xDzANBgNVBAoMBkh1YXdlaTETMBEGA1UECwwKSHVhd2VpIENCRzEeMBwGA1UE
AwwVSHVhd2VpIENCRyBSb290IENBIEcyMB4XDTIwMDMxNjAzMDQzOVoXDTQ5MDMx
NjAzMDQzOVowUzELMAkGA1UEBhMCQ04xDzANBgNVBAoMBkh1YXdlaTETMBEGA1UE
CwwKSHVhd2VpIENCRzEeMBwGA1UEAwwVSHVhd2VpIENCRyBSb290IENBIEcyMHYw
EAYHKoZIzj0CAQYFK4EEACIDYgAEWidkGnDSOw3/HE2y2GHl+fpWBIa5S+IlnNrs
GUvwC1I2QWvtqCHWmwFlFK95zKXiM8s9yV3VVXh7ivN8ZJO3SC5N1TCrvB2lpHMB
wcz4DA0kgHCMm/wDec6kOHx1xvCRo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T
AQH/BAUwAwEB/zAdBgNVHQ4EFgQUo45a9Vq8cYwqaiVyfkiS4pLcIAAwCgYIKoZI
zj0EAwMDZwAwZAIwMypeB7P0IbY7c6gpWcClhRznOJFj8uavrNu2PIoz9KIqr3jn
BlBHJs0myI7ntYpEAjBbm8eDMZY5zq5iMZUC6H7UzYSix4Uy1YlsLVV738PtKP9h
FTjgDHctXJlC5L7+ZDY=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDATCCAoigAwIBAgIIXmuDXbWpOB8wCgYIKoZIzj0EAwMwUzELMAkGA1UEBhMC
Q04xDzANBgNVBAoMBkh1YXdlaTETMBEGA1UECwwKSHVhd2VpIENCRzEeMBwGA1UE
AwwVSHVhd2VpIENCRyBSb290IENBIEcyMB4XDTIwMDcwOTAyMDQyNFoXDTMwMDcw
NzAyMDQyNFowYjELMAkGA1UEBgwCQ04xDzANBgNVBAoMBkh1YXdlaTETMBEGA1UE
CwwKSHVhd2VpIENCRzEtMCsGA1UEAwwkSHVhd2VpIENCRyBEZXZlbG9wZXIgUmVs
YXRpb25zIENBIEcyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE65LdoIZh1hlpZ2gP
bJ6gPhHsvYSRe22KETgdqeVeYnrbRHI9wsPT6RGYS+pU4mPl6wxzgDMqN6SY/BoZ
luhkE1PzaHoPoNIWIq0O33hpyKyyYwAacIUEjYurkw1E9r9no4IBGDCCARQwHwYD
VR0jBBgwFoAUo45a9Vq8cYwqaiVyfkiS4pLcIAAwHQYDVR0OBBYEFNtek7Ij6NDk
/nF6Zumkc0dbf/NeMEYGA1UdIAQ/MD0wOwYEVR0gADAzMDEGCCsGAQUFBwIBFiVo
dHRwOi8vY3BraS1jYXdlYi5odWF3ZWkuY29tL2Nwa2kvY3BzMBIGA1UdEwEB/wQI
MAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMGYGA1UdHwRfMF0wW6BZoFeGVWh0dHA6
Ly9jcGtpLWNhd2ViLmh1YXdlaS5jb20vY3BraS9zZXJ2bGV0L2NybEZpbGVEb3du
LmNybD9jZXJ0eXBlPTEwJi9yb290X2cyX2NybC5jcmwwCgYIKoZIzj0EAwMDZwAw
ZAIwWO1X5q2MdfpR1Q237GpUHGbL1C13rGyFg2p3AYo44FpZ2/A9ss0wOHKM4KDl
ZPqdAjBLkf8NPZy7KVog98+iCTLq35DJ2ZVxkCxknA9YhiHVyXf4HPm4JlT7rW7o
Q+FzM3c=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDEDCCApagAwIBAgIOCfvzQsZnJptzoZmQEgcwCgYIKoZIzj0EAwMwYjELMAkG
A1UEBgwCQ04xDzANBgNVBAoMBkh1YXdlaTETMBEGA1UECwwKSHVhd2VpIENCRzEt
MCsGA1UEAwwkSHVhd2VpIENCRyBEZXZlbG9wZXIgUmVsYXRpb25zIENBIEcyMB4X
DTI1MDMyNDA4MDczNloXDTI4MDMyNDA4MDczNlowgb8xCzAJBgNVBAYTAkNOMTkw
NwYDVQQKDDDmlbDlrZflpKnloILvvIjljJfkuqzvvInnvZHnu5zmioDmnK/mnInp
mZDlhazlj7gxHDAaBgNVBAsMEzEyNTA2Njg4MTI5NjYwNTcxNTMxVzBVBgNVBAMM
TuaVsOWtl+WkqeWggu+8iOWMl+S6rO+8iee9kee7nOaKgOacr+aciemZkOWFrOWP
uCgxMjUwNjY4ODEyOTY2MDU3MTUzKVwsUmVsZWFzZTBZMBMGByqGSM49AgEGCCqG
SM49AwEHA0IABLuSjYWlLYk30lvBR7o3vrnJYMgnq+23Z9Qh7OJwXo4XCYNP1Pdx
VFTx9LTz8rEqoNLpnlzWzJQhtIHxDfnOhZejgdEwgc4wHQYDVR0OBBYEFDYoxuRj
vKfOEnSHdku5bV7OlRQDMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAU216TsiPo
0OT+cXpm6aRzR1t/814wWQYDVR0fBFIwUDBOoEygSoZIaHR0cDovL2g1aG9zdGlu
Zy1kcmNuLmRiYW5rY2RuLmNuL2NjaDUvY3JsL2hkcmNhZzIvSHVhd2VpQ0JHSERS
RzJjcmwuY3JsMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAK
BggqhkjOPQQDAwNoADBlAjEAzOpq9+LmKBmeEPYi8sHVFt7zPrpq08x2MGY6vZp8
wMTR6nwXvEwvUai6SHOn+SR3AjAM9ut0DHE9M6VSldiYegDhtp9n766pCvZYSUAy
9JN0SZMKNnJQ0/Lp2vsw73mh3Zg=
-----END CERTIFICATE-----

查看文件

@@ -1,163 +0,0 @@
{
"module": {
"name": "entry",
"type": "entry",
"description": "$string:module_desc",
"mainElement": "EntryAbility",
"deviceTypes": [
"phone",
"tablet",
"2in1"
],
"querySchemes": [
"mailto",
"weixin"
],
"deliveryWithInstall": true,
"installationFree": false,
"pages": "$profile:main_pages",
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "$string:EntryAbility_desc",
"icon": "$media:layered_image",
"label": "$string:EntryAbility_label",
"startWindowIcon": "$media:startIcon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": [
"entity.system.home"
],
"actions": [
"action.system.home"
]
},
{
"actions": [
"ohos.want.action.viewData"
],
"uris": [
{
"scheme": "hellouniappx",
"host": "redirect"
},
{
"scheme": "uniappxhello",
"host": "redirect"
}
]
}
],
"preferMultiWindowOrientation": "landscape_auto"
}
],
"metadata": [
{
"name": "TENCENT_MAP_KEY",
"value": ""
}
],
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
},
{
"name": "ohos.permission.APPROXIMATELY_LOCATION",
"reason": "$string:permission_location_tips",
"usedScene": {
"abilities": [
"EntryAbility"
],
"when": "inuse"
}
},
{
"name": "ohos.permission.LOCATION",
"reason": "$string:permission_location_tips",
"usedScene": {
"abilities": [
"EntryAbility"
],
"when": "inuse"
}
},
{
"name": "ohos.permission.GET_NETWORK_INFO",
"reason": "$string:permission_get_networkinfo_tips",
"usedScene": {
"abilities": [
"EntryAbility"
],
"when": "inuse"
}
},
{
"name": "ohos.permission.MICROPHONE",
"reason": "$string:permission_microphone",
"usedScene": {
"abilities": [
"EntryAbility"
],
"when": "inuse"
}
},
{
"name": "ohos.permission.LOCATION_IN_BACKGROUND",
"reason": "$string:permission_location_tips",
"usedScene": {
"abilities": [
"EntryAbility"
],
"when": "inuse"
}
},
// {
// "name": "ohos.permission.READ_PASTEBOARD",
// "reason": "$string:permission_read_pasteboard",
// "usedScene": {
// "abilities": [
// "EntryAbility"
// ],
// "when": "inuse"
// }
// },
{
"name": "ohos.permission.PRIVACY_WINDOW",
"reason": "$string:permission_read_privacywindow",
"usedScene": {
"abilities": [
"EntryAbility"
],
"when": "inuse"
}
},
{
"name": "ohos.permission.ACCESS_BIOMETRIC",
"reason": "$string:permission_read_accessbiometric",
"usedScene": {
"abilities": [
"EntryAbility"
],
"when": "inuse"
}
},
{
"name": "ohos.permission.ACCELEROMETER"
},
{
"name": "ohos.permission.GYROSCOPE"
},
{
"name": "ohos.permission.WRITE_CALENDAR",
"reason": "$string:write_calendar_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
]
}
}

查看文件

@@ -1,215 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>本地网页</title>
<style type="text/css">
.btn {
display: block;
margin: 20px auto;
padding: 5px;
background-color: #007aff;
border: 0;
color: #ffffff;
height: 40px;
width: 200px;
}
.btn-red {
background-color: #dd524d;
}
.desc {
padding: 10px;
color: #999999;
}
</style>
</head>
<body>
<p class="desc">web-view 组件加载本地 html 示例,仅在 App 环境下生效。点击下列按钮,跳转至其它页面。</p>
<div class="btn-list">
<button class="btn" type="button" data-action="navigateTo">navigateTo</button>
<button class="btn" type="button" data-action="redirectTo">redirectTo</button>
<button class="btn" type="button" data-action="navigateBack">navigateBack</button>
<button class="btn" type="button" data-action="reLaunch">reLaunch</button>
<button class="btn" type="button" data-action="switchTab">switchTab</button>
<button class="btn" type="button" data-action="getEnv">getEnv</button>
<!-- <button class="btn" type="button" data-action="getTheme">获取主题</button> -->
</div>
<p class="desc">网页向应用发送消息。注意:小程序端应用会在此页面后退时接收到消息。</p>
<div class="btn-list">
<button class="btn btn-red" type="button" id="postMessage">postMessage</button>
</div>
<p class="desc"></p>本地视频</p>
<video id="videoLocal" src="../../static/test-video/10second-demo.mp4" controls style="width: 100%;"></video>
<p class="desc"></p>网络视频</p>
<video id="videoWeb" src="https://qiniu-web-assets.dcloud.net.cn/video/sample/2minute-demo.mp4" controls style="width: 100%;"></video>
<video id="videoCamera" style="width: 100%;"></video>
<div class="btn-list">
<button class="btn" type="button" data-action="openCamera">打开摄像头</button>
<button class="btn" type="button" data-action="closeCamera">关闭摄像头</button>
<button class="btn" type="button" data-action="getLocation">获取定位</button>
</div>
<p class="desc">文件选择</p>
<div style="padding: 0 10px;">
<p style="font-size: 14px;">&lt;input type="file" /&gt;</p>
<input type="file" />
<p style="font-size: 14px;">&lt;input type="file" accept="image/*" multiple /&gt;</p>
<input type="file" accept="image/*" multiple />
<p style="font-size: 14px;">普通input</p>
<input placeholder="底部输入框" />
<p style="font-size: 14px;">普通input2</p>
<input placeholder="底部输入框2" />
<p style="font-size: 14px;">普通input3</p>
<input placeholder="底部输入框3" />
<br />
<p style="font-size: 14px;">普通textarea</p>
<textarea placeholder="底部textarea"></textarea>
<br />
<a href="https://web-ext-storage.dcloud.net.cn/uni-app-x/pkg/hello-uniapp-x.apk" download>hello uni-app x
apk下载(自动化测试使用)</a>
<iframe width="100%" height="250px" srcdoc="<p>iframe中输入框示例</p>
<p style=&quot;font-size: 14px;&quot;>普通input</p>
<input placeholder=&quot;底部输入框&quot; />
<p style=&quot;font-size: 14px;&quot;>普通input2</p>
<input placeholder=&quot;底部输入框2&quot; />
<p style=&quot;font-size: 14px&quot;>普通input3</p>
<input placeholder=&quot;底部输入框3&quot; />
<p style=&quot;font-size: 14px;&quot;>普通textarea</p>
<textarea placeholder=&quot;底部textarea&quot;></textarea>">
</iframe>
</div>
<!-- uni 的 SDK -->
<script type="text/javascript" src="uni.webview.1.5.5.js"></script>
<script type="text/javascript">
function postMessage(action) {
uni.webView.postMessage({
data: {
action
}
});
}
const isWeb = location.href && /^https?:/.test(location.href)
// document.addEventListener('UniAppJSBridgeReady', function() {
document.querySelectorAll('.btn-list').forEach(btnList => {
btnList.addEventListener('click', function(evt) {
console.log('按钮点击事件触发');
if (isWeb) {
alert('web端暂不支持此功能')
return
}
console.log('evt: ',evt);
var target = evt.target;
console.log('target: ', target);
if (target.tagName === 'BUTTON') {
var action = target.getAttribute('data-action');
console.log(action);
switch (action) {
case 'switchTab':
uni.webView.switchTab({
url: '/pages/tabBar/API'
});
break;
case 'reLaunch':
uni.webView.reLaunch({
url: '/pages/tabBar/component'
});
break;
case 'navigateBack':
uni.webView.navigateBack({
delta: 1
});
break;
case 'getEnv':
uni.webView.getEnv((res) => {
postMessage(res)
});
break;
/* case 'getTheme':
alert(getThemePreference())
break; */
case 'getLocation':
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
function(position) {
const locationData = {
latitude: position.coords.latitude,
longitude: position.coords.longitude,
accuracy: position.coords.accuracy
};
console.log('position: ',JSON.stringify(locationData));
alert(JSON.stringify(locationData));
},
function(error) {
console.error('获取位置失败:', error.message);
alert('获取位置失败: ' + error.message);
},
{
enableHighAccuracy: true,
timeout: 5000,
maximumAge: 0
}
);
} else {
alert('您的浏览器不支持地理定位功能');
}
break;
case 'openCamera':
var constraints = {audio: true,video: {facingMode: "user"}};
navigator.mediaDevices
.getUserMedia(constraints)
.then(function(mediaStream) {
var video = document.getElementById("videoCamera");
video.srcObject = mediaStream;
video.onloadedmetadata = function(e) {
video.play();
};
})
.catch(function(err) {
console.log(err.name + ": " + err.message);
}); // 总是在最后检查错误
break;
case 'closeCamera':
var video = document.getElementById("videoCamera");
if (video.srcObject) {
video.srcObject.getTracks().forEach(function(track) {
track.stop();
});
video.srcObject = null; // 清除视频源
video.load(); // 重新加载视频元素
}
break;
default:
uni.webView[action]({
url: '/pages/component/button/button'
});
break;
}
}
})
})
// });
document.querySelector("#postMessage").addEventListener('click', function() {
if (isWeb) {
alert('web端暂不支持此功能')
return
}
postMessage('message')
})
/* function getThemePreference() {
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
} else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches) {
return 'light';
}
return 'unknown';
} */
</script>
</body>
</html>

文件差异内容过多而无法显示 加载差异

查看文件

@@ -1,17 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>本地网页</title>
<style type="text/css">
.desc {
padding: 10px;
color: #999999;
}
</style>
</head>
<body>
<p class="desc">web-view 组件加载本地简单 html 示例(内容不可滚动),用于测试 scroll-view 嵌套滚动。</p>
</body>
</html>

文件差异因一行或多行过长而隐藏

查看文件

@@ -1,216 +0,0 @@
const path = require("path");
const fs = require("fs");
const {
configureToMatchImageSnapshot
} = require('jest-image-snapshot');
const MATCH_IMAGE_SNAPSHOTS_DIR = "__image_snapshots_for_match__";
const toMatchImageSnapshotBase = configureToMatchImageSnapshot({
customSnapshotIdentifier(args) {
return args.currentTestName.replace(/\//g, "-").replace(" ", "-");
},
});
expect.extend({
toMatchImageSnapshot,
toSaveSnapshot,
toSaveImageSnapshot,
});
const testCaseToSnapshotFilePath =
process.env.testCaseToSnapshotFilePath || "./testCaseToSnapshotFilePath.json";
if (!fs.existsSync(testCaseToSnapshotFilePath)) {
fs.writeFileSync(testCaseToSnapshotFilePath, "{}");
}
function toMatchImageSnapshot(received, options = {}) {
const {
testPath,
} = this;
const snapshotOptions = {
...options,
customSnapshotsDir:
options.customSnapshotsDir ||
process.env.UNI_BASE_SNAPSHOT_DEVICE_DIR ||
createSnapshotDir({
testPath,
SNAPSHOTS_DIR: MATCH_IMAGE_SNAPSHOTS_DIR,
}),
customDiffDir: options.customDiffDir || path.join(
getMatchImageSnapshotDir(),
"diff"
),
};
return toMatchImageSnapshotBase.call(this, received, snapshotOptions);
}
function getMatchImageSnapshotDir() {
return process.env.UNI_BASE_SNAPSHOT_DEVICE_DIR || path.join(__dirname, "__snapshot__");
}
function writeTestCaseToSnapshotFile(testCaseName, snapshotFilePath) {
const data = JSON.parse(fs.readFileSync(testCaseToSnapshotFilePath));
if (testCaseName.includes(__dirname)) {
testCaseName = testCaseName.substring(`${__dirname}`.length);
if (testCaseName[0] == '/' || testCaseName[0] == '\\') {
testCaseName = testCaseName.substring(1);
};
};
if (!data[testCaseName]) {
data[testCaseName] = [snapshotFilePath];
} else {
data[testCaseName].push(snapshotFilePath);
}
fs.writeFileSync(testCaseToSnapshotFilePath, JSON.stringify(data, null, 2));
}
function toSaveSnapshot(received, {
customSnapshotsDir,
fileName
} = {}) {
const {
snapshotState: {
_rootDir
},
testPath,
currentTestName,
} = this;
const SNAPSHOTS_DIR = "__file_snapshots__";
const snapshotDir =
process.env.saveSnapshotDir ||
createSnapshotDir({
customSnapshotsDir,
testPath,
SNAPSHOTS_DIR,
});
const _fileName = createFileName({
fileName,
testPath,
currentTestName,
});
const filePath = path.join(snapshotDir, _fileName);
let message = () => `${currentTestName} toSaveSnapshot success`;
let pass = true;
try {
checkSnapshotDir(path.dirname(filePath));
fs.writeFileSync(filePath, received);
writeTestCaseToSnapshotFile(testPath.replace(`${_rootDir}/`, ""), filePath);
} catch (e) {
console.log("toSaveSnapshot fail", e);
message = () => e.message;
pass = false;
}
return {
message,
pass,
};
}
function toSaveImageSnapshot(
received, {
customSnapshotsDir,
customSnapshotIdentifier
} = {}
) {
const {
snapshotState: {
_rootDir
},
testPath,
currentTestName,
} = this;
const SNAPSHOTS_DIR = "__image_snapshots__";
const snapshotDir =
process.env.saveImageSnapshotDir ||
createSnapshotDir({
customSnapshotsDir,
testPath,
SNAPSHOTS_DIR,
});
const _fileName = createFileName({
fileName: customSnapshotIdentifier ? `${customSnapshotIdentifier()}.png` : "",
testPath,
currentTestName,
fileType: "png",
});
const filePath = path.join(snapshotDir, _fileName);
let message = () => `${currentTestName} toSaveImageSnapshot success`;
let pass = true;
try {
checkSnapshotDir(path.dirname(filePath));
fs.writeFileSync(filePath, Buffer.from(received, "base64"));
writeTestCaseToSnapshotFile(testPath.replace(`${_rootDir}/`, ""), filePath);
} catch (e) {
console.log("toSaveImageSnapshot fail", e);
message = () => e.message;
pass = false;
}
return {
message,
pass,
};
}
function createSnapshotDir({
customSnapshotsDir,
testPath,
SNAPSHOTS_DIR
}) {
return customSnapshotsDir || path.join(path.dirname(testPath), SNAPSHOTS_DIR);
}
function createFileName({
fileName,
testPath,
currentTestName,
fileType
}) {
return (
fileName ||
createSnapshotIdentifier({
testPath,
currentTestName,
fileType,
})
);
}
function createSnapshotIdentifier({
testPath,
currentTestName,
fileType = "txt",
}) {
const snapshotIdentifier = kebabCase(
`${path.basename(testPath)}-${currentTestName}`
);
const counter = timesCalled.get(`${snapshotIdentifier}-${fileType}`) || 1;
timesCalled.set(`${snapshotIdentifier}-${fileType}`, counter + 1);
return `${snapshotIdentifier}-${counter}.${fileType}`;
}
function kebabCase(str) {
return str
.replaceAll(/([a-z])([A-Z])/g, "$1-$2")
.replaceAll(/\s+/g, "-")
.replaceAll(/_+/g, "-")
.replaceAll(/\/+/g, "-")
.replaceAll(/\.+/g, "-")
.toLowerCase();
}
function checkSnapshotDir(snapshotDir) {
if (!fs.existsSync(snapshotDir)) {
fs.mkdirSync(snapshotDir, {
recursive: true,
});
}
}
const timesCalled = new Map();

查看文件

@@ -1,44 +0,0 @@
const fs = require('fs')
const path = require('path')
const config = {
testTimeout: 15000,
reporters: ['default'],
watchPathIgnorePatterns: ['/node_modules/', '/dist/', '/.git/'],
moduleFileExtensions: ['js', 'json'],
rootDir: __dirname,
testMatch: ['<rootDir>/pages/**/*test.[jt]s?(x)'],
testPathIgnorePatterns: ['/node_modules/', '<rootDir>/pages/uni-ui/'],
setupFilesAfterEnv: ['<rootDir>/jest-setup.js'],
testSequencer: path.join(__dirname, "testSequencer.js")
}
// 如果主机电脑中有虚拟网卡(比如安装了 Docker/WSL 等软件),额外的 IP 地址有可能干扰应用端通过 IP 地址连接到主机端的测试工具。
// 如果存在这种情况,可以在当前目录下创建一个 globalSetup.js 文件,在其中检测到实际用于网络通信的主 IP 地址并传递给应用端。
// globalSetup.js 的内容可参考下面这段代码:
//
// const dgram = require('dgram');
// function getPrimaryIP() {
// return new Promise((resolve, reject) => {
// const socket = dgram.createSocket('udp4');
// // 这里并不会真的发送数据包,只是触发系统路由选择
// socket.connect(80, '8.8.8.8', () => {
// const { address } = socket.address();
// socket.close();
// resolve(address);
// });
// socket.on('error', (err) => {
// resolve(undefined);
// });
// });
// }
// module.exports = async () => {
// process.env.UNI_AUTOMATOR_HOST = await getPrimaryIP()
// };
const globalSetupPath = path.resolve(__dirname, 'globalSetup.js')
if (fs.existsSync(globalSetupPath)) {
config.globalSetup = globalSetupPath;
}
module.exports = config;

查看文件

@@ -1,37 +1,9 @@
// 仅测试 console.log 时机问题
import './test-main-console.uts'
// #ifdef APP-ANDROID || APP-IOS || WEB || MP-WEIXIN || APP-HARMONY
import { uniStat } from '@/uni_modules/uni-stat/plugin.uts'
// #endif
import App from './App.uvue'
import { createSSRApp } from 'vue'
// 统计配置
const uniStatcollectItems = {
uniStatPageLog: true
}
const uniStatOptions = {
enable: process.env.NODE_ENV == 'production',
debug: false,
collectItems: uniStatcollectItems,
}
export function createApp() {
const app = createSSRApp(App)
// #ifdef APP-ANDROID || APP-IOS || WEB || MP-WEIXIN || APP-HARMONY
app.use(uniStat, uniStatOptions)
// #endif
// app.mixin({
// onReady() {
// setTimeout(() => {
// console.log((this as BasePage).$nativePage!.getDomJson())
// }, 100)
// }
// })
return {
app
}
}
// 此注释必须放在此文件的最后一行,用于测试issue#18520

查看文件

@@ -1,17 +1,13 @@
{
"id": "hello-uniapp-x",
"id": "omnicard-ai",
"name": "OmniCard",
"displayName": "OmniCard",
"version": "1.0.44",
"description": "OmniCard",
"scripts": {
"check-commit": "node ./git-hooks/check-commit.cjs"
},
"repository": "https://gitcode.net/dcloud/hello-uni-app-x",
"version": "1.0.0",
"description": "OmniCard AI 用户端:数字名片、移动微官网、访客雷达与 NFC 管家",
"scripts": {},
"keywords": [
"uni-app-x",
"uni-app2.0",
"示例工程"
"OmniCard"
],
"author": "",
"license": "MIT",
@@ -44,8 +40,8 @@
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
"tcb": "n",
"aliyun": "n"
},
"client": {
"App": {
@@ -54,20 +50,20 @@
"app-uvue": "y"
},
"H5-mobile": {
"Safari": "n",
"Android Browser": "n",
"微信浏览器(Android)": "n",
"QQ浏览器(Android)": "n"
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "n",
"Chrome": "y",
"IE": "n",
"Edge": "n",
"Firefox": "n",
"Safari": "n"
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "n",
"微信": "y",
"阿里": "n",
"百度": "n",
"字节跳动": "n",
@@ -88,4 +84,4 @@
}
}
}
}
}

查看文件

@@ -43,10 +43,17 @@
"backgroundColorContent": "#F7F8FA"
}
},
{
"path": "pages/owner/hall/index",
"style": {
"navigationBarTitleText": "名片大厅",
"backgroundColorContent": "#F7F8FA"
}
},
{
"path": "pages/owner/card/index",
"style": {
"navigationBarTitleText": "名片中心",
"navigationBarTitleText": "我的名片",
"backgroundColorContent": "#F7F8FA"
}
},
@@ -60,7 +67,7 @@
{
"path": "pages/owner/leads",
"style": {
"navigationBarTitleText": "潜客列表",
"navigationBarTitleText": "客户管理",
"backgroundColorContent": "#F7F8FA"
}
},

查看文件

@@ -1,28 +0,0 @@
## pages.json
创建 `component`、`API`、`CSS` 示例页面时,如果该示例页面需要在对应 `tabBar` 菜单中展示,`path` 命名需要遵循以下规则:
- pages/component/component-name/component-name
- pages/API/api-name/api-name
- pages/CSS/css-name/css-name
所以,如果是扩展示例,比如针对 `button type 属性` 的示例,`path` 可以是:`pages/component/button/button-type`\
如果想要该示例页面在菜单中显示,则需要调整为:`pages/component/button/button/type`\
如果 API 示例为多个 API 的组合,此时无法以单一 API 名称命名,可以使用 `pages/API/xxx/xxx` 命名示例页面,但在维护 [syntaxdoc](http://git.dcloud.io/uni-app-x/syntaxdoc) `modules.json` 时,需要通过 `items` 节点关联对应 API 信息。\
增加上述示例页面时,相同类型的页面要放在一起,不要随意放置在最后或最前!\
即:\
pages/component/xxx/xxx 的页面应该相邻放置,\
pages/API/xxx/xxx 的页面应该相邻放置,\
pages/CSS/xxx/xxx 的页面应该相邻放置,\
而不是把新增的页面放在 pages 数组的最后或最前。
增加上述示例页面时,不需要基于平台兼容性补充条件编译,代码提交后,会基于 [syntaxdoc](http://git.dcloud.io/uni-app-x/syntaxdoc) 仓库中的平台兼容性信息,自动生成对应的条件编译代码,并更新 `pages.json`。
`pages.json` 中增加后页面配置后,需要在 [syntaxdoc](http://git.dcloud.io/uni-app-x/syntaxdoc) 仓库的 `modules.json` 中维护目录信息。
如需在本地调试时增加 `tabBar` 页面入口,需要修改 `pages.json` 中 `page` 的 `group` 信息,格式为使用 `,` 分隔的数字字符串。\
`page.group` 属性中的数字代表该页面在 `pages.json/groups` 中对应位置的下标,
`pages.json/groups` 中维护了基于 `syntaxdoc/modules.json` 获取的目录信息。
调整现有页面的路径或平台兼容性,或移除页面时,如果该页面涉及截图对比测试,需要同时调整 `pages/pages.test.js` 中的页面地址。

查看文件

@@ -25,8 +25,15 @@
<text class="brand-subtitle">{{ t('subtitle') }}</text>
</view>
<!-- 2. 极简分段控制器 (Segmented Control) -->
<!-- 2. 极简分段控制器 (Segmented Control: 账号密码 / 手机号 / 邮箱) -->
<view class="segmented-box">
<view
class="segment-item"
:class="{ 'segment-active': state.tab === 'password' }"
@click="handleSwitchTab('password')"
>
<text class="segment-text">{{ t('tabPassword') }}</text>
</view>
<view
class="segment-item"
:class="{ 'segment-active': state.tab === 'phone' }"
@@ -44,8 +51,35 @@
</view>
<!-- 3. 表单主体 -->
<view v-if="state.tab === 'phone'" class="form-section">
<!-- 手机号与区号一体化输入组 -->
<!-- 3.1 账号密码真实登录面板 (便于测试与前后端对齐) -->
<view v-if="state.tab === 'password'" class="form-section">
<view class="form-field">
<text class="field-label">{{ t('usernamePlaceholder') }}</text>
<input
class="input-control text-input"
type="text"
v-model="state.username"
:placeholder="t('usernamePlaceholder')"
:disabled="state.submitting"
/>
</view>
<view class="form-field">
<text class="field-label">{{ t('passwordPlaceholder') }}</text>
<input
class="input-control text-input"
type="password"
v-model="state.password"
:placeholder="t('passwordPlaceholder')"
:disabled="state.submitting"
/>
</view>
<view class="test-account-badge" @click="handleAutofillTest">
<text class="test-account-text">{{ t('testAccountTip') }}</text>
</view>
</view>
<!-- 3.2 国际手机号验证码面板 -->
<view v-else-if="state.tab === 'phone'" class="form-section">
<view class="form-field">
<text class="field-label">{{ t('tabPhone') }}</text>
<view class="phone-group">
@@ -63,7 +97,6 @@
</view>
</view>
<!-- 行内一体化验证码输入行 -->
<view class="form-field">
<text class="field-label">{{ t('smsCode') }}</text>
<view class="code-group">
@@ -85,8 +118,8 @@
</view>
</view>
<!-- 3.3 邮箱 MagicLink 面板 -->
<view v-else class="form-section">
<!-- 邮箱输入行 -->
<view class="form-field">
<text class="field-label">{{ t('tabEmail') }}</text>
<input
@@ -108,6 +141,8 @@
<button
class="primary-cta-btn"
:class="{ 'primary-cta-disabled': state.submitting }"
role="button"
aria-label="登录"
:disabled="state.submitting"
@click="handlePrimarySubmit"
>
@@ -154,12 +189,14 @@ import { getCurrentLocale, isRtlLocale, OmniLocale, setAppLocale } from '../../c
import {
createLoginState,
switchLoginTab,
submitPasswordLogin,
submitPhoneLogin,
sendMagicLink,
sendSmsCode,
tickCountdown,
tapSocialLogin,
resolveLoginText,
fillTestAccount,
LoginCopyKey,
LoginTab,
SocialProvider,
@@ -201,6 +238,9 @@ const codeButtonText = computed((): string => {
});
const primaryButtonText = computed((): string => {
if (state.tab === 'password') {
return state.submitting ? t('loading') : t('login');
}
return state.tab === 'phone' ? t('login') : t('magicLink');
});
@@ -224,14 +264,14 @@ function handleGoBack(): void {
try {
uni.navigateBack({
fail: () => {
uni.switchTab({
url: '/pages/portal/index',
uni.reLaunch({
url: '/pages/owner/hall/index',
});
},
});
} catch (_e) {
uni.switchTab({
url: '/pages/portal/index',
uni.reLaunch({
url: '/pages/owner/hall/index',
});
}
}
@@ -266,8 +306,14 @@ function handleSendCode(): void {
}
}
function handleAutofillTest(): void {
fillTestAccount(state);
}
function handlePrimarySubmit(): void {
if (state.tab === 'phone') {
if (state.tab === 'password') {
submitPasswordLogin(state);
} else if (state.tab === 'phone') {
submitPhoneLogin(state);
} else {
sendMagicLink(state);
@@ -576,6 +622,32 @@ onUnmounted(() => {
color: #98A2B3;
}
/* 测试账号一键填充辅助胶囊 */
.test-account-badge {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 6px 10px;
background-color: #F7F8FA;
border: 1px dashed #D0D5DD;
border-radius: 8px;
margin-bottom: 12px;
cursor: pointer;
transition: background-color 150ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.test-account-badge:active {
background-color: #EAF1FF;
border-color: #0B5CFF;
}
.test-account-text {
font-size: 12px;
color: #0B5CFF;
font-weight: 500;
}
/* 错误提示 */
.error-banner {
padding: 8px 12px;
@@ -593,8 +665,13 @@ onUnmounted(() => {
.primary-cta-btn {
width: 100%;
height: 44px;
line-height: 44px;
min-height: 44px;
background-color: #0B5CFF;
color: #FFFFFF;
font-size: 15px;
font-weight: 600;
text-align: center;
border-radius: 12px;
display: flex;
align-items: center;
@@ -602,15 +679,21 @@ onUnmounted(() => {
border: none;
margin-top: 6px;
margin-bottom: 18px;
cursor: pointer;
transition: opacity 150ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.primary-cta-btn::after {
border: none;
}
.primary-cta-btn:active {
opacity: 0.88;
}
.primary-cta-disabled {
background-color: #E3E7ED;
cursor: not-allowed;
}
.primary-cta-text {

查看文件

@@ -1,12 +1,14 @@
/**
* OmniCard AI (CRMmp) - 统一身份认证登录业务驱动层
* 第一版可用骨架:不接真实 Google / Apple SDK,不伪造成功登录
* 第一版可用骨架:支持账号密码真实登录对齐后端、手机号验证码、邮箱MagicLink与社交登录入口
*/
import { getCurrentLocale, getOmniText, OmniLocale } from '../../common/locale.uts';
import { setAuthToken, setTenantId } from '../../common/request.uts';
import { setAuthToken, setTenantId, httpPost } from '../../common/request.uts';
import { collectHallContact } from '../../common/api.uts';
import { consumePendingCollectCard } from '../owner/hall/useHall.uts';
export type LoginTab = 'phone' | 'email';
export type LoginTab = 'password' | 'phone' | 'email';
export type SocialProvider = 'google' | 'apple';
export type LoginCopyKey =
@@ -14,8 +16,14 @@ export type LoginCopyKey =
| 'brandTag'
| 'subtitle'
| 'desc'
| 'tabPassword'
| 'tabPhone'
| 'tabEmail'
| 'usernamePlaceholder'
| 'passwordPlaceholder'
| 'accountRequired'
| 'passwordRequired'
| 'testAccountTip'
| 'countryCode'
| 'phonePlaceholder'
| 'sendCode'
@@ -39,6 +47,8 @@ export type LoginCopyKey =
export type LoginState = {
tab: LoginTab;
username: string;
password: string;
countryCode: string;
phone: string;
smsCode: string;
@@ -48,6 +58,13 @@ export type LoginState = {
countdown: number;
};
export type LoginAdminResponse = {
token: string;
expire?: number;
refreshToken?: string;
refreshExpire?: number;
};
function isZhLocale(): boolean {
return getCurrentLocale() === 'zh-CN';
}
@@ -71,12 +88,30 @@ export function resolveLoginText(key: LoginCopyKey | string, targetLocale?: stri
if (key === 'subtitle') {
return zh ? '极简出海商务,连接全球商机' : 'Minimalist global commerce & identity';
}
if (key === 'tabPassword') {
return zh ? '账号密码' : 'Password';
}
if (key === 'tabPhone') {
return zh ? '手机号' : 'Phone';
}
if (key === 'tabEmail') {
return zh ? '邮箱' : 'Email';
}
if (key === 'usernamePlaceholder') {
return zh ? '用户名或手机号' : 'Username or phone';
}
if (key === 'passwordPlaceholder') {
return zh ? '登录密码' : 'Password';
}
if (key === 'accountRequired') {
return zh ? '请填写账号' : 'Enter your username';
}
if (key === 'passwordRequired') {
return zh ? '请填写密码' : 'Enter your password';
}
if (key === 'testAccountTip') {
return zh ? '测试账号: admin / 123456 (点击一键填入)' : 'Test account: admin / 123456 (Click to autofill)';
}
if (key === 'desc') {
return zh ? '使用国际手机号或邮箱继续' : 'Continue with your phone or email';
}
@@ -148,20 +183,51 @@ function toastNone(title: string): void {
}
function finishAfterLogin(): void {
if (typeof uni.switchTab === 'function') {
uni.switchTab({
url: '/pages/owner/card/index',
});
} else if (typeof uni.reLaunch === 'function') {
uni.reLaunch({
url: '/pages/owner/card/index',
try {
uni.showToast({
title: resolveLoginText('loginSuccess'),
icon: 'success',
});
} catch (e) {}
const pendingCardId = consumePendingCollectCard();
if (pendingCardId !== '') {
collectHallContact(pendingCardId).catch(() => {});
}
try {
if (typeof uni.switchTab === 'function') {
uni.switchTab({
url: '/pages/owner/card/index',
});
if (typeof window !== 'undefined' && window.location != null) {
try {
setTimeout(() => {
const currentHash = window.location.hash || '';
if (!currentHash.includes('/pages/owner/card/index')) {
window.location.hash = '#/pages/owner/card/index';
}
}, 300);
} catch (e) {}
}
return;
}
} catch (e) {}
try {
if (typeof uni.reLaunch === 'function') {
uni.reLaunch({
url: '/pages/owner/card/index',
});
}
} catch (e) {}
}
export function createLoginState(): LoginState {
return {
tab: 'phone',
tab: 'password',
username: '',
password: '',
countryCode: '+86',
phone: '',
smsCode: '',
@@ -177,6 +243,15 @@ export function switchLoginTab(state: LoginState, tab: LoginTab): void {
state.errorMessage = '';
}
/**
* 一键填充测试账号
*/
export function fillTestAccount(state: LoginState): void {
state.username = 'admin';
state.password = '123456';
state.errorMessage = '';
}
/**
* 倒计时推进函数
*/
@@ -206,6 +281,52 @@ export function completeLogin(token: string, tenantId?: string): boolean {
return true;
}
/**
* 账号密码真实登录:对齐后端 Cool-Admin POST /admin/base/open/login
*/
export async function submitPasswordLogin(state: LoginState): Promise<boolean> {
const username = state.username != null ? state.username.trim() : '';
if (username === '') {
state.errorMessage = resolveLoginText('accountRequired');
toastNone(state.errorMessage);
return false;
}
const password = state.password != null ? state.password.trim() : '';
if (password === '') {
state.errorMessage = resolveLoginText('passwordRequired');
toastNone(state.errorMessage);
return false;
}
if (state.submitting) {
return false;
}
state.submitting = true;
state.errorMessage = '';
try {
const res = await httpPost<LoginAdminResponse>('/admin/base/open/login', {
username,
password,
});
state.submitting = false;
if (res != null && res.token != null && res.token !== '') {
completeLogin(res.token);
return true;
}
state.errorMessage = '登录失败,未获取到有效凭证';
toastNone(state.errorMessage);
return false;
} catch (err: any) {
state.submitting = false;
const msg = err && err.message ? err.message : '账户或密码不正确~';
state.errorMessage = msg;
return false;
}
}
export function sendSmsCode(state: LoginState): boolean {
if (state.countdown > 0) {
return false;

查看文件

@@ -30,7 +30,7 @@
<list-view v-else class="card-home-list" :scroll-y="true">
<list-item v-for="item in state.list" :key="item.id">
<view class="card-row" @click="handleOpenEdit(item)">
<view class="card-row" @click="handleOpenPreview(item)">
<view class="card-main">
<text class="card-name">{{ item.name }}</text>
<text class="card-title">{{ formatField(item.title) }}</text>
@@ -38,7 +38,7 @@
</view>
<view class="card-meta">
<text class="card-status">{{ statusLabelOf(item.status) }}</text>
<text class="card-poster" @click.stop="handleOpenPoster(item)">{{ posterLabel }}</text>
<text class="card-poster" @click.stop="handleOpenEdit(item)">{{ editLabel }}</text>
</view>
</view>
</list-item>
@@ -59,13 +59,17 @@
<script setup lang="uts">
import { getCurrentLocale, getOmniText } from '../../../common/locale.uts';
import { fetchCardQuota } from '../../../common/api.uts';
import { getTenantId } from '../../../common/request.uts';
import { utsLightColors, utsRadius, utsSpacing } from '../../../common/tokens.uts';
import {
applyCardHomeOptions,
createCardHomeState,
formatCardField,
openCardEdit,
openCardPreview,
openCardQrcode,
pickShareCard,
refreshCards,
resolveCardHomeText,
statusLabel,
@@ -94,7 +98,7 @@ const state = reactive(createCardHomeState());
const pageTitle = ref(resolveCardHomeText('title'));
const pageSubtitle = ref(resolveCardHomeText('subtitle'));
const searchPlaceholder = ref(resolveCardHomeText('searchPlaceholder'));
const posterLabel = ref(getOmniText(getCurrentLocale(), 'card.share'));
const editLabel = ref(getOmniText(getCurrentLocale(), 'common.edit'));
const ctaLabel = computed((): string => {
if (state.loading) {
@@ -122,17 +126,35 @@ function handleOpenEdit(item: CardHomeItem): void {
openCardEdit(item.id);
}
function handleOpenPoster(item: CardHomeItem): void {
openCardQrcode(item);
function handleOpenPreview(item: CardHomeItem): void {
openCardPreview(item);
}
async function handleRefresh(): Promise<void> {
const shareCard = pickShareCard(state.list);
if (shareCard != null) {
openCardQrcode(shareCard);
return;
}
await refreshCards(state);
}
async function applyQuotaHint(): Promise<void> {
try {
const tenantId = getTenantId();
const quota = await fetchCardQuota(tenantId !== '' ? tenantId : 1);
if (quota.exceeded === true) {
pageSubtitle.value = resolveCardHomeText('quotaExceeded');
} else {
pageSubtitle.value = resolveCardHomeText('subtitle');
}
} catch (_e) {}
}
onLoad((options: CardHomePageOptions) => {
applyCardHomeOptions(state, options);
refreshCards(state);
applyQuotaHint();
});
</script>

查看文件

@@ -26,13 +26,16 @@ export type CardHomeCopyKey =
| 'subtitle'
| 'searchPlaceholder'
| 'refresh'
| 'share'
| 'preview'
| 'poster'
| 'empty'
| 'error'
| 'loading'
| 'statusDraft'
| 'statusPublished'
| 'statusArchived';
| 'statusArchived'
| 'quotaExceeded';
export type LoadCardsQuery = {
page: number;
@@ -83,9 +86,12 @@ function isZhLocale(): boolean {
*/
export function resolveCardHomeText(key: CardHomeCopyKey): string {
const locale = getCurrentLocale();
if (key === 'poster') {
if (key === 'poster' || key === 'share') {
return getOmniText(locale, 'card.share');
}
if (key === 'preview') {
return getOmniText(locale, 'card.view_profile');
}
if (key === 'empty') {
return getOmniText(locale, 'common.empty');
}
@@ -98,16 +104,16 @@ export function resolveCardHomeText(key: CardHomeCopyKey): string {
const zh = isZhLocale();
if (key === 'title') {
return zh ? '名片中心' : 'Cards';
return zh ? '我的名片' : 'My cards';
}
if (key === 'subtitle') {
return zh ? '编辑资料或生成分享海报' : 'Edit a card or open its poster';
return zh ? '预览微官网或分享当前名片' : 'Preview the site or share a card';
}
if (key === 'searchPlaceholder') {
return zh ? '搜索姓名、职位或公司' : 'Search name, title or company';
}
if (key === 'refresh') {
return zh ? '刷新名片' : 'Refresh cards';
return zh ? '分享名片' : 'Share card';
}
if (key === 'statusDraft') {
return zh ? '草稿' : 'Draft';
@@ -118,6 +124,9 @@ export function resolveCardHomeText(key: CardHomeCopyKey): string {
if (key === 'statusArchived') {
return zh ? '已归档' : 'Archived';
}
if (key === 'quotaExceeded') {
return zh ? '名片配额已满,升级后可继续创建' : 'Card quota is full. Upgrade to create more.';
}
return '';
}
@@ -311,3 +320,31 @@ export function openCardQrcode(card: CardHomeItem): void {
url: buildQrcodeUrl(card),
});
}
export function buildPortalPreviewUrl(card: CardHomeItem): string {
if (card.slug != null && card.slug !== '') {
return `/pages/portal/index?slug=${card.slug}`;
}
if (card.id != null) {
return `/pages/portal/index?id=${card.id}`;
}
return '/pages/portal/index';
}
export function openCardPreview(card: CardHomeItem): void {
uni.navigateTo({
url: buildPortalPreviewUrl(card),
});
}
export function pickShareCard(list: CardHomeItem[]): CardHomeItem | null {
for (let i = 0; i < list.length; i++) {
if (list[i].status === 'published') {
return list[i];
}
}
if (list.length > 0) {
return list[0];
}
return null;
}

查看文件

@@ -0,0 +1,368 @@
<template>
<view class="hall-page" :style="pageTheme">
<view class="hall-header">
<text class="hall-title">{{ pageTitle }}</text>
<text class="hall-subtitle">{{ pageSubtitle }}</text>
<view class="hall-tabs">
<view
class="hall-tab"
:class="{ 'hall-tab-active': state.tab === 'contacts' }"
@click="handleTab('contacts')"
>
<text class="hall-tab-text">{{ t('tabContacts') }}</text>
</view>
<view
class="hall-tab"
:class="{ 'hall-tab-active': state.tab === 'team' }"
@click="handleTab('team')"
>
<text class="hall-tab-text">{{ t('tabTeam') }}</text>
</view>
</view>
<input
class="hall-search"
type="text"
v-model="state.keyWord"
:placeholder="searchPlaceholder"
:disabled="state.loading"
/>
<input
v-if="state.tab === 'contacts'"
class="hall-search"
type="number"
maxlength="6"
v-model="state.exchangeCode"
:placeholder="exchangePlaceholder"
:disabled="state.loading"
/>
</view>
<view v-if="state.loading" class="hall-skeleton">
<view class="skeleton-line"></view>
<view class="skeleton-card"></view>
<view class="skeleton-card"></view>
</view>
<view v-else-if="state.errorMessage !== ''" class="hall-feedback">
<text class="feedback-title">{{ t('error') }}</text>
<text class="feedback-desc">{{ state.errorMessage }}</text>
</view>
<view v-else-if="state.empty" class="hall-feedback">
<text class="feedback-title">{{ t('empty') }}</text>
<text class="feedback-desc">{{ emptyDesc }}</text>
</view>
<list-view v-else class="hall-list" :scroll-y="true">
<list-item v-for="item in state.list" :key="item.peerCardId">
<view class="hall-row" @click="handleOpenPortal(item)">
<view class="hall-main">
<text class="hall-name">{{ item.peerName }}</text>
<text class="hall-meta-text">{{ formatField(item.peerTitle) }}</text>
<text class="hall-meta-text">{{ formatField(item.peerCompany) }}</text>
</view>
</view>
</list-item>
</list-view>
<view class="hall-cta-bar">
<button
class="primary-cta-btn"
:class="{ 'primary-cta-btn-disabled': state.loading }"
:disabled="state.loading"
@click="handlePrimary"
>
<text class="primary-cta-btn-text">{{ ctaLabel }}</text>
</button>
</view>
</view>
</template>
<script setup lang="uts">
import { utsLightColors, utsRadius, utsSpacing } from '../../../common/tokens.uts';
import {
applyHallTab,
createHallState,
HallContactItem,
HallCopyKey,
HallTab,
openHallPortal,
refreshHall,
resolveHallText,
submitHallExchange,
} from './useHall.uts';
const pageTheme: Record<string, string> = {
'--oc-primary': utsLightColors.primary,
'--oc-primary-soft': utsLightColors.primarySoft,
'--oc-bg': utsLightColors.bg,
'--oc-surface': utsLightColors.surface,
'--oc-border': utsLightColors.border,
'--oc-text': utsLightColors.text,
'--oc-text-2': utsLightColors.text2,
'--oc-radius-btn': utsRadius.button,
'--oc-radius-card': utsRadius.card,
'--oc-space-8': utsSpacing.space8,
'--oc-space-12': utsSpacing.space12,
'--oc-space-16': utsSpacing.space16,
'--oc-space-24': utsSpacing.space24,
};
const state = reactive(createHallState());
const pageTitle = ref(resolveHallText('title'));
const pageSubtitle = ref(resolveHallText('subtitle'));
const searchPlaceholder = ref(resolveHallText('searchPlaceholder'));
const exchangePlaceholder = ref(resolveHallText('exchangePlaceholder'));
const emptyDesc = computed((): string => {
if (state.tab === 'team') {
return resolveHallText('teamEmpty');
}
return pageSubtitle.value;
});
const ctaLabel = computed((): string => {
if (state.loading) {
return resolveHallText('loading');
}
if (state.exchangeCode.trim() !== '') {
return resolveHallText('exchange');
}
return resolveHallText('refresh');
});
function t(key: HallCopyKey): string {
return resolveHallText(key);
}
function formatField(value: string | null | undefined): string {
if (value == null) {
return '';
}
return value;
}
async function handleTab(tab: HallTab): Promise<void> {
applyHallTab(state, tab);
await refreshHall(state);
}
function handleOpenPortal(item: HallContactItem): void {
openHallPortal(item);
}
async function handlePrimary(): Promise<void> {
if (state.exchangeCode.trim() !== '') {
await submitHallExchange(state);
return;
}
await refreshHall(state);
}
onShow(() => {
refreshHall(state);
});
</script>
<style scoped>
.hall-page {
display: flex;
flex-direction: column;
height: 100%;
background-color: var(--oc-surface, #F7F8FA);
}
.hall-header {
display: flex;
flex-direction: column;
padding-inline: var(--oc-space-16, 16px);
padding-block-start: var(--oc-space-16, 16px);
padding-block-end: var(--oc-space-12, 12px);
background-color: var(--oc-bg, #FFFFFF);
border-bottom: 1px solid var(--oc-border, #E3E7ED);
}
.hall-title {
font-size: 24px;
font-weight: 700;
color: var(--oc-text, #101828);
line-height: 1.25;
margin-bottom: var(--oc-space-8, 8px);
}
.hall-subtitle {
font-size: 14px;
color: var(--oc-text-2, #667085);
line-height: 1.6;
margin-bottom: var(--oc-space-12, 12px);
}
.hall-tabs {
display: flex;
flex-direction: row;
margin-bottom: var(--oc-space-12, 12px);
}
.hall-tab {
min-height: 44px;
min-width: 44px;
padding-inline: 16px;
margin-inline-end: 8px;
border: 1px solid var(--oc-border, #E3E7ED);
border-radius: 12px;
background-color: var(--oc-bg, #FFFFFF);
display: flex;
align-items: center;
justify-content: center;
}
.hall-tab-active {
background-color: var(--oc-primary-soft, #EAF1FF);
border-color: var(--oc-primary, #0B5CFF);
}
.hall-tab-text {
font-size: 13px;
font-weight: 600;
color: var(--oc-text-2, #667085);
}
.hall-tab-active .hall-tab-text {
color: var(--oc-primary, #0B5CFF);
}
.hall-search {
height: 44px;
padding-inline: 12px;
margin-bottom: var(--oc-space-12, 12px);
background-color: var(--oc-surface, #F7F8FA);
border: 1px solid var(--oc-border, #E3E7ED);
border-radius: var(--oc-radius-btn, 12px);
font-size: 16px;
color: var(--oc-text, #101828);
}
.hall-skeleton {
flex: 1;
padding-inline: var(--oc-space-16, 16px);
padding-block: var(--oc-space-16, 16px);
}
.skeleton-line {
height: 16px;
width: 40%;
background-color: var(--oc-border, #E3E7ED);
border-radius: 8px;
margin-bottom: var(--oc-space-12, 12px);
}
.skeleton-card {
height: 88px;
background-color: var(--oc-border, #E3E7ED);
border-radius: var(--oc-radius-card, 16px);
margin-bottom: var(--oc-space-12, 12px);
}
.hall-feedback {
flex: 1;
margin: 40px var(--oc-space-16, 16px);
padding: var(--oc-space-24, 24px);
background-color: var(--oc-bg, #FFFFFF);
border: 1px solid var(--oc-border, #E3E7ED);
border-radius: var(--oc-radius-card, 16px);
display: flex;
flex-direction: column;
align-items: center;
}
.feedback-title {
font-size: 16px;
font-weight: 600;
color: var(--oc-text, #101828);
margin-bottom: var(--oc-space-8, 8px);
}
.feedback-desc {
font-size: 14px;
color: var(--oc-text-2, #667085);
text-align: center;
line-height: 1.6;
}
.hall-list {
flex: 1;
padding-inline: var(--oc-space-16, 16px);
padding-block-start: var(--oc-space-12, 12px);
padding-block-end: calc(88px + var(--window-bottom, 0px) + env(safe-area-inset-bottom, 0px));
}
.hall-row {
min-height: 88px;
padding: var(--oc-space-16, 16px);
margin-bottom: var(--oc-space-12, 12px);
background-color: var(--oc-bg, #FFFFFF);
border: 1px solid var(--oc-border, #E3E7ED);
border-radius: var(--oc-radius-card, 16px);
display: flex;
flex-direction: row;
align-items: center;
}
.hall-main {
display: flex;
flex-direction: column;
flex: 1;
}
.hall-name {
font-size: 16px;
font-weight: 600;
color: var(--oc-text, #101828);
line-height: 1.25;
margin-bottom: 4px;
}
.hall-meta-text {
font-size: 14px;
color: var(--oc-text-2, #667085);
line-height: 1.6;
}
.hall-cta-bar {
position: fixed;
left: 0;
right: 0;
bottom: var(--window-bottom, 0px);
padding: 12px var(--oc-space-16, 16px) calc(12px + env(safe-area-inset-bottom, 0px)) var(--oc-space-16, 16px);
background-color: var(--oc-bg, #FFFFFF);
border-top: 1px solid var(--oc-border, #E3E7ED);
box-sizing: border-box;
z-index: 100;
}
.primary-cta-btn {
height: 48px;
min-height: 44px;
background-color: var(--oc-primary, #0B5CFF);
border-radius: var(--oc-radius-btn, 12px);
display: flex;
align-items: center;
justify-content: center;
width: 100%;
border-width: 0;
}
.primary-cta-btn-disabled {
background-color: var(--oc-border, #E3E7ED);
}
.primary-cta-btn-text {
color: var(--oc-bg, #FFFFFF);
font-size: 15px;
font-weight: 600;
}
.primary-cta-btn-disabled .primary-cta-btn-text {
color: var(--oc-text-2, #667085);
}
</style>

查看文件

@@ -0,0 +1,273 @@
/**
* OmniCard AI - 名片大厅人脉驱动层
*/
import {
CardContact,
OwnerPageData,
OwnerPageQuery,
} from '../../../common/types.uts';
import {
collectHallContact,
confirmHallExchange,
fetchHallContactPage,
fetchTeamWallPage,
} from '../../../common/api.uts';
import { getCurrentLocale, getOmniText } from '../../../common/locale.uts';
export type HallCopyKey =
| 'title'
| 'subtitle'
| 'searchPlaceholder'
| 'refresh'
| 'exchangePlaceholder'
| 'exchange'
| 'empty'
| 'error'
| 'loading'
| 'scan'
| 'tabContacts'
| 'tabTeam'
| 'teamEmpty';
export type HallContactItem = {
id?: string | number;
peerCardId: number;
peerName: string;
peerTitle?: string;
peerCompany?: string;
peerSlug?: string;
source?: string;
};
export type HallTab = 'contacts' | 'team';
export type HallState = {
tab: HallTab;
list: HallContactItem[];
page: number;
size: number;
total: number;
keyWord: string;
exchangeCode: string;
loading: boolean;
empty: boolean;
errorMessage: string;
};
export type LoadHallApi = {
fetchHallContactPage: (
query: OwnerPageQuery
) => Promise<OwnerPageData<CardContact>>;
fetchTeamWallPage?: (
query: OwnerPageQuery
) => Promise<OwnerPageData<CardContact>>;
confirmHallExchange?: (
exchangeCode: string
) => Promise<{ status: string; isDualSynced: boolean }>;
collectHallContact?: (peerCardId: string | number) => Promise<CardContact>;
};
function isZhLocale(): boolean {
return getCurrentLocale() === 'zh-CN';
}
export function resolveHallText(key: HallCopyKey): string {
const locale = getCurrentLocale();
if (key === 'empty') {
return getOmniText(locale, 'common.empty');
}
if (key === 'error') {
return getOmniText(locale, 'common.error');
}
if (key === 'loading') {
return getOmniText(locale, 'common.loading');
}
if (key === 'scan') {
return getOmniText(locale, 'card.scan_nfc');
}
const zh = isZhLocale();
if (key === 'title') {
return zh ? '名片大厅' : 'Card hall';
}
if (key === 'subtitle') {
return zh ? '已互换和收藏的名片' : 'Exchanged and saved cards';
}
if (key === 'searchPlaceholder') {
return zh ? '搜索姓名或公司' : 'Search name or company';
}
if (key === 'refresh') {
return zh ? '刷新人脉' : 'Refresh network';
}
if (key === 'exchangePlaceholder') {
return zh ? '输入 6 位互换码' : '6-digit exchange code';
}
if (key === 'exchange') {
return zh ? '确认互换' : 'Confirm exchange';
}
if (key === 'tabContacts') {
return zh ? '人脉' : 'Network';
}
if (key === 'tabTeam') {
return zh ? '团队' : 'Team';
}
if (key === 'teamEmpty') {
return zh ? '还没有同事发布名片' : 'No published teammate cards yet';
}
return '';
}
export function toHallItem(row: CardContact): HallContactItem {
return {
id: row.id,
peerCardId: Number(row.peerCardId),
peerName: row.peerName != null ? row.peerName : '',
peerTitle: row.peerTitle,
peerCompany: row.peerCompany,
peerSlug: row.peerSlug,
source: row.source,
};
}
export function createHallState(): HallState {
return {
tab: 'contacts',
list: [],
page: 1,
size: 20,
total: 0,
keyWord: '',
exchangeCode: '',
loading: false,
empty: false,
errorMessage: '',
};
}
function readErrorMessage(err: unknown): string {
if (err instanceof Error && err.message !== '') {
return err.message;
}
return resolveHallText('error');
}
export function buildHallPortalUrl(item: HallContactItem): string {
if (item.peerSlug != null && item.peerSlug !== '') {
return `/pages/portal/index?slug=${item.peerSlug}`;
}
return `/pages/portal/index?id=${item.peerCardId}`;
}
export function openHallPortal(item: HallContactItem): void {
uni.navigateTo({
url: buildHallPortalUrl(item),
});
}
export async function refreshHall(
state: HallState,
api?: LoadHallApi
): Promise<void> {
if (state.loading) {
return;
}
state.loading = true;
state.errorMessage = '';
const fetchPage =
state.tab === 'team'
? api != null && api.fetchTeamWallPage != null
? api.fetchTeamWallPage
: fetchTeamWallPage
: api != null
? api.fetchHallContactPage
: fetchHallContactPage;
try {
const query: OwnerPageQuery = {
page: state.page,
size: state.size,
};
const word = state.keyWord.trim();
if (word !== '') {
query.keyWord = word;
}
const data = await fetchPage(query);
const raw = data.list != null ? data.list : [];
const list: HallContactItem[] = [];
for (let i = 0; i < raw.length; i++) {
list.push(toHallItem(raw[i]));
}
state.list = list;
state.total =
data.pagination != null ? data.pagination.total : list.length;
state.empty = list.length === 0;
} catch (err) {
state.list = [];
state.empty = false;
state.errorMessage = readErrorMessage(err);
}
state.loading = false;
}
export async function submitHallExchange(
state: HallState,
api?: LoadHallApi
): Promise<boolean> {
const code = state.exchangeCode.trim();
if (!/^\d{6}$/.test(code)) {
state.errorMessage = resolveHallText('exchangePlaceholder');
return false;
}
const confirm =
api != null && api.confirmHallExchange != null
? api.confirmHallExchange
: confirmHallExchange;
try {
await confirm(code);
state.exchangeCode = '';
await refreshHall(state, api);
return true;
} catch (err) {
state.errorMessage = readErrorMessage(err);
return false;
}
}
export async function collectPeerCard(
peerCardId: string | number,
api?: LoadHallApi
): Promise<boolean> {
const collect =
api != null && api.collectHallContact != null
? api.collectHallContact
: collectHallContact;
await collect(peerCardId);
return true;
}
export function applyHallTab(state: HallState, tab: HallTab): void {
state.tab = tab === 'team' ? 'team' : 'contacts';
state.page = 1;
state.errorMessage = '';
}
export const PENDING_COLLECT_KEY = 'omni_pending_collect_card';
export function setPendingCollectCard(cardId: string | number): void {
try {
uni.setStorageSync(PENDING_COLLECT_KEY, String(cardId));
} catch (_e) {}
}
export function consumePendingCollectCard(): string {
try {
const raw = uni.getStorageSync(PENDING_COLLECT_KEY);
uni.removeStorageSync(PENDING_COLLECT_KEY);
if (raw == null) {
return '';
}
return String(raw).trim();
} catch (_e) {
return '';
}
}

查看文件

@@ -3,6 +3,12 @@
<view class="leads-header">
<text class="leads-title">{{ pageTitle }}</text>
<text class="leads-subtitle">{{ followUpLabel }}</text>
<view v-if="radarLineOne !== ''" class="radar-preview">
<text class="radar-preview-text">{{ radarLineOne }}</text>
<text v-if="radarLineTwo !== ''" class="radar-preview-text">{{ radarLineTwo }}</text>
<text v-if="radarLineThree !== ''" class="radar-preview-text">{{ radarLineThree }}</text>
<text class="overview-entry" @click="openRadar">{{ radarMoreLabel }}</text>
</view>
<text class="overview-entry" @click="openTeamOverview">{{ briefingLabel }}</text>
<input
class="leads-search"
@@ -110,6 +116,8 @@ import {
applyLeadsOptions,
createLeadsState,
formatLeadCompany,
formatRadarLine,
openRadarPage,
refreshLeads,
resolveLeadsText,
statusLabel,
@@ -135,11 +143,30 @@ const pageTheme: Record<string, string> = {
};
const state = reactive(createLeadsState());
const pageTitle = ref(getOmniText(getCurrentLocale(), 'crm.leads'));
const pageTitle = ref(resolveLeadsText('title'));
const followUpLabel = ref(getOmniText(getCurrentLocale(), 'crm.follow_up'));
const searchPlaceholder = ref(resolveLeadsText('searchPlaceholder'));
const scoreLabel = ref(resolveLeadsText('score'));
const briefingLabel = ref(resolveOverviewText('title'));
const radarMoreLabel = ref(resolveLeadsText('radarMore'));
const radarLineOne = computed((): string => {
if (state.radarTracks.length < 1) {
return '';
}
return formatRadarLine(state.radarTracks[0]);
});
const radarLineTwo = computed((): string => {
if (state.radarTracks.length < 2) {
return '';
}
return formatRadarLine(state.radarTracks[1]);
});
const radarLineThree = computed((): string => {
if (state.radarTracks.length < 3) {
return '';
}
return formatRadarLine(state.radarTracks[2]);
});
const ctaLabel = computed((): string => {
if (state.loading) {
@@ -170,6 +197,10 @@ function handleOpenLead(item: LeadEntity): void {
});
}
function openRadar(): void {
openRadarPage();
}
function openTeamOverview(): void {
uni.navigateTo({
url: '/pages/owner/team/overview',
@@ -219,6 +250,22 @@ onLoad((options: LeadsPageOptions) => {
margin-bottom: var(--oc-space-8, 8px);
}
.radar-preview {
margin-bottom: var(--oc-space-12, 12px);
padding: var(--oc-space-12, 12px);
background-color: var(--oc-surface, #F7F8FA);
border: 1px solid var(--oc-border, #E3E7ED);
border-radius: var(--oc-radius-card, 16px);
}
.radar-preview-text {
display: block;
font-size: 13px;
color: var(--oc-text-2, #667085);
line-height: 1.6;
margin-bottom: 4px;
}
.overview-entry {
min-height: 44px;
font-size: 14px;

查看文件

@@ -44,6 +44,36 @@
</view>
</view>
<view class="section-card">
<text class="section-title">{{ t('workspaceSection') }}</text>
<view class="setting-row" @click="openPage('/pages/owner/nfc/scan')">
<text class="setting-label">{{ t('nfcScan') }}</text>
</view>
<view class="row-divider"></view>
<view class="setting-row" @click="openPage('/pages/owner/nfc/mode')">
<text class="setting-label">{{ t('nfcMode') }}</text>
</view>
<view class="row-divider"></view>
<view class="setting-row" @click="openPage('/pages/owner/nfc/security')">
<text class="setting-label">{{ t('nfcSecurity') }}</text>
</view>
<view class="row-divider"></view>
<view class="setting-row" @click="openPage('/pages/owner/team/overview')">
<text class="setting-label">{{ t('teamBriefing') }}</text>
</view>
</view>
<view class="section-card">
<text class="section-title">{{ t('quotaSection') }}</text>
<view class="setting-row">
<view class="setting-text-wrap">
<text class="setting-label">{{ t('quotaCards') }}</text>
<text class="setting-desc">{{ quotaLine }}</text>
</view>
</view>
<text v-if="quotaExceeded" class="setting-desc">{{ t('quotaUpgrade') }}</text>
</view>
<!-- Notification settings section -->
<view class="section-card">
<text class="section-title">{{ t('notificationsSection') }}</text>
@@ -139,6 +169,8 @@ import {
removeAccountAvatar,
} from './useProfile.uts';
import { OmniLocale } from '../../../common/locale.uts';
import { fetchCardQuota } from '../../../common/api.uts';
import { getTenantId } from '../../../common/request.uts';
type LangOption = {
code: OmniLocale;
@@ -146,6 +178,8 @@ type LangOption = {
};
const state = reactive<ProfileState>(loadProfileSettings());
const quotaLine = ref('--');
const quotaExceeded = ref(false);
function onAvatarClick(): void {
if (state.account.avatar !== '') {
@@ -197,6 +231,24 @@ function onSelectLocale(code: OmniLocale): void {
setPreferredLocale(state, code);
}
function openPage(url: string): void {
uni.navigateTo({
url,
});
}
async function loadQuota(): Promise<void> {
try {
const tenantId = getTenantId();
const quota = await fetchCardQuota(tenantId !== '' ? tenantId : 1);
quotaLine.value = `${quota.used} / ${quota.limit}`;
quotaExceeded.value = quota.exceeded === true;
} catch (_e) {
quotaLine.value = '--';
quotaExceeded.value = false;
}
}
function onSaveSettings(): void {
state.saving = true;
const ok = saveProfileSettings(state);
@@ -209,11 +261,12 @@ function onSaveSettings(): void {
}
}
onMounted(() => {
onShow(() => {
const loaded = loadProfileSettings();
state.account = loaded.account;
state.notifications = loaded.notifications;
state.locale = loaded.locale;
loadQuota();
});
</script>

查看文件

@@ -384,6 +384,22 @@ export function resolveProfileText(key: string): string {
return isZh ? '偏好设置' : 'Preferences';
case 'languageLabel':
return isZh ? '界面语言' : 'Display Language';
case 'workspaceSection':
return isZh ? '工作台' : 'Workspace';
case 'nfcScan':
return isZh ? 'NFC 贴卡激活' : 'Activate NFC card';
case 'nfcMode':
return isZh ? 'NFC 场景切换' : 'NFC scene mode';
case 'nfcSecurity':
return isZh ? 'NFC 挂失' : 'Freeze NFC card';
case 'teamBriefing':
return isZh ? '展会战报' : 'Team briefing';
case 'quotaSection':
return isZh ? '套餐配额' : 'Plan quota';
case 'quotaCards':
return isZh ? '名片用量' : 'Card usage';
case 'quotaUpgrade':
return isZh ? '已达上限,请升级套餐' : 'Limit reached. Upgrade to continue.';
default:
return key;
}

查看文件

@@ -8,9 +8,11 @@ import {
LeadStatus,
OwnerPageData,
OwnerPageQuery,
RadarTrack,
} from '../../common/types.uts';
import { fetchOwnerLeadPage } from '../../common/api.uts';
import { fetchOwnerLeadPage, fetchOwnerRadarPage } from '../../common/api.uts';
import { getCurrentLocale, getOmniText } from '../../common/locale.uts';
import { formatLocation, formatStay } from './useRadar.uts';
export type LeadsCopyKey =
| 'title'
@@ -26,7 +28,8 @@ export type LeadsCopyKey =
| 'empty'
| 'error'
| 'loading'
| 'score';
| 'score'
| 'radarMore';
export type LoadLeadsQuery = {
page: number;
@@ -54,6 +57,7 @@ export type LoadLeadsResult = {
export type LeadsState = {
list: LeadEntity[];
radarTracks: RadarTrack[];
page: number;
size: number;
total: number;
@@ -147,6 +151,9 @@ export function resolveLeadsText(key: LeadsCopyKey): string {
if (key === 'score') {
return zh ? '意向分' : 'Intent';
}
if (key === 'radarMore') {
return zh ? '查看全部雷达' : 'Full radar';
}
return '';
}
@@ -236,6 +243,7 @@ export async function loadLeads(
export function createLeadsState(): LeadsState {
return {
list: [],
radarTracks: [],
page: 1,
size: 20,
total: 0,
@@ -304,10 +312,31 @@ export async function refreshLeads(
api
);
applyLoadResult(state, result);
try {
const radar = await fetchOwnerRadarPage({ page: 1, size: 3 });
state.radarTracks = radar.list != null ? radar.list : [];
} catch (_err) {
state.radarTracks = [];
}
state.loading = false;
return result;
}
export function formatRadarLine(track: RadarTrack): string {
const place = formatLocation(track.country, track.city);
const stay = formatStay(track.stayDurationMs);
if (place !== '') {
return `${place} · ${stay}`;
}
return stay;
}
export function openRadarPage(): void {
uni.navigateTo({
url: '/pages/owner/radar',
});
}
export function buildLeadDetailUrl(id: string | number): string {
return `/pages/owner/leads-detail?id=${id}`;
}

查看文件

@@ -5,6 +5,7 @@
<i18n-selector :model-value="currentLang" @change="switchLang" />
</view>
<text class="locale-chip locale-exchange" @click="openExchange">{{ textOf('card.exchange') }}</text>
<text v-if="canCollect" class="locale-chip locale-exchange" @click="handleCollect">{{ collectLabel }}</text>
</view>
<scroll-view class="portal-scroll" :scroll-y="true">
@@ -185,6 +186,9 @@ import {
PortalQuickAction,
} from './usePortal.uts';
import { saveToContacts } from '../../common/vcard-export.uts';
import { collectHallContact } from '../../common/api.uts';
import { getAuthToken } from '../../common/request.uts';
import { setPendingCollectCard } from '../owner/hall/useHall.uts';
import {
OmniLocale,
OmniMessageKey,
@@ -222,6 +226,10 @@ const defaultAvatar = 'https://cdn.omnicard.ai/assets/default-avatar.png';
const state = reactive(createPortalState());
const currentCardId = ref('');
const currentLang = ref<OmniLocale>(detectLocale());
const canCollect = ref(true);
const collectLabel = computed((): string => {
return currentLang.value === 'zh-CN' ? '收入大厅' : 'Save to hall';
});
const cardAudioUrl = computed((): string => {
if (state.card == null) {
@@ -309,6 +317,26 @@ function handleCompanyClick(): void {
});
}
async function handleCollect(): Promise<void> {
if (state.card == null || state.card!.id == null) {
return;
}
if (getAuthToken() === '') {
setPendingCollectCard(state.card!.id);
uni.navigateTo({
url: '/pages/common/login',
});
return;
}
try {
await collectHallContact(state.card!.id);
uni.showToast({
title: currentLang.value === 'zh-CN' ? '已收入大厅' : 'Saved to hall',
icon: 'none',
});
} catch (_err) {}
}
function openExchange(): void {
if (!state.card) return;
const cardId = state.card!.id != null ? String(state.card!.id) : currentCardId.value;

查看文件

@@ -1,229 +0,0 @@
隐私政策
最近更新日期:2025年04月09日
本政策仅适用于数字天堂(北京)网络技术有限公司的DCloud开发者中心系统应用产品或服务。
本政策将帮助您了解以下内容:
1. 我们如何收集和使用您的用户信息
2. 我们如何使用 Cookie 和同类技术
3. 我们如何共享、转让、公开披露您的用户信息
4. 我们如何保护您的用户信息
5. 您的权利
6. 我们如何处理儿童的个人信息
7. 您的用户信息如何储存及如何在全球范围转移
8. 本政策如何更新
9. 如何联系我们
我们深知用户信息对您的重要性,并会尽全力保护您的用户信息安全可靠。我们致力于维持您对我们的信任,恪守以下原则,保护您的用户信息:权责一致原则、目的明确原则、选择同意原则、最少够用原则、确保安全原则、主体参与原则、公开透明原则等。同时,我们承诺,我们将按业界成熟的安全标准,采取相应的安全保护措施来保护您的用户信息。
请在使用我们的产品(或服务)前,仔细阅读并了解本隐私政策。
一、我们如何收集和使用您的用户信息
我们的产品基于DCloud uni-app x开发,应用运行期间需要收集您的设备唯一识别码(ODID/OAID)以提供统计分析服务,并通过应用启动数据及异常错误日志分析改进性能和用户体验,为用户提供更好的服务。详情内容请访问《DCloud用户服务条款》。
(一)**我们可能会申请以下权限,仅用于演示 uni-app x 框架的功能**
- **网络**
- **位置**
- **存储**
- **相册**
- **摄像头**
- **麦克风**
- **蓝牙**
- **联系人**
- **日历**
- **隐私窗口**
- **读取剪贴板**
- **生物特征识别能力**
(二)**征得授权同意的例外**
请您理解,根据法律法规及相关国家标准,以下情形中,我们收集和使用您的用户信息无需征得您的授权同意:
1. **与国家安全、国防安全直接相关的;**
2. **与公共安全、公共卫生、重大公共利益直接相关的;**
3. **与犯罪侦查、起诉、审判和判决执行等直接相关的;**
4. **出于维护您或其他个人的生命、财产等重大合法权益但又很难得到本人同意的;**
5. **所收集的您的用户信息是您自行向社会公众公开的;**
6. **从合法公开披露的信息中收集的您的用户信息,如合法的新闻报道、政府信息公开等渠道;**
7. **根据您的要求签订或履行合同所必需的;**
8. **用于维护软件及相关服务的安全稳定运行所必需的,例如发现、处置软件及相关服务的故障;**
9. **个人信息控制者为新闻单位且其在开展合法的新闻报道所必需的;**
10. **学术研究机构基于公共利益开展统计或学术研究所必要,且对外提供学术研究或描述的结果时,对结果中所包含的个人信息进行去标识化处理的。**
11. **法律法规规定的其他情形。**
二、我们如何使用 Cookie 和同类技术
(一)Cookie
为确保网站正常运转,我们会在您的计算机或移动设备上存储名为 Cookie 的小数据文件。Cookie 通常包含标识符、站点名称以及一些号码和字符。借助于 Cookie,网站能够存储您的访问偏好数据。
我们不会将 Cookie 用于本政策所述目的之外的任何用途。您可根据自己的偏好管理或删除 Cookie。您可以清除计算机上保存的所有 Cookie,大部分网络浏览器都设有阻止Cookie 的功能。但如果您这么做,则需要在每一次访问我们的网站时亲自更改用户设置。
(二)网站信标和像素标签
除 Cookie 外,我们还会在网站上使用网站信标和像素标签等其他同类技术。例如,我们向您发送的电子邮件可能含有链接至我们网站内容的点击URL。
如果您点击该链接,我们则会跟踪此次点击,帮助我们了解您的产品或服务偏好并改善客户服务。网站信标通常是一种嵌入到网站或电子邮件中的透明图像。借助于电子邮件中的像素标签,我们能够获知电子邮件是否被打开。如果您不希望自己的活动以这种方式被追踪,则可以随时从我们的寄信名单中退订。
(三)Do Not Track(请勿追踪)
很多网络浏览器均设有Do Not Track功能,该功能可向网站发布Do Not Track请求。目前,主要互联网标准组织尚未设立相关政策来规定网站应如何应对此类请求。但如果您的浏览器启用了 Do Not Track,那么我们的所有网站都会尊重您的选择。
三、我们如何共享、转让、公开披露您的用户信息
(一)共享
我们不会与其他的任何公司、组织和个人分享您的用户信息,但以下情况除外:
1. 在获取明确同意的情况下共享:获得您的明确同意后,我们会与其他方共享您的用户信息。
2. 我们可能会根据法律法规规定,或按政府主管部门的强制性要求,对外共享您的用户信息。
3. 与我们的关联公司共享:您的用户信息可能会与我们的关联公司共享。我们只会共享必要的用户信息,且受本隐私政策中所声明目的的约束。关联公司如要改变用户信息的处理目的,将再次征求您的授权同意。
4. 与授权合作伙伴共享:仅为实现本政策中声明的目的,我们的某些服务将由授权合作伙伴提供。我们可能会与合作伙伴共享您的某些用户信息,以提供更好的客户服务和用户体验。我们仅会出于合法、正当、必要、特定、明确的目的共享您的用户信息,并且只会共享提供服务所必要的用户信息。为了更好运营和改善技术和服务,您同意我们和授权合作伙伴在符合相关法律法规的前提下可将收集的信息用于其他服务和用途。
(二)转让
我们不会将您的用户信息转让给任何公司、组织和个人,但以下情况除外:
1. 在获取明确同意的情况下转让:获得您的明确同意后,我们会向其他方转让您的用户信息;
2. 在涉及合并、收购或破产清算时,如涉及到用户信息转让,我们会再要求新的持有您用户信息的公司、组织继续受此隐私政策的约束,否则我们将要求该公司、组织重新向您征求授权同意。
(三)公开披露
我们仅会在以下情况下,公开披露您的用户信息:
1. 获得您明确同意后;
2. 基于法律的披露:在法律、法律程序、诉讼或政府主管部门强制性要求的情况下,我们可能会公开披露您的用户信息。
(四)共享、转让、公开披露信息时事先征得授权同意的例外
请您理解,根据法律法规及相关国家标准,以下情形中,我们共享、转让、公开披露您的用户信息无需征得您的授权同意:
1. 与国家安全、国防安全直接相关的;
2. 与公共安全、公共卫生、重大公共利益直接相关的;
3. 与犯罪侦查、起诉、审判和判决执行等直接相关的;
4. 出于维护您或其他个人的生命、财产等重大合法权益但又很难得到本人同意的;
5. 您自行向社会公众公开的信息;
6. 从合法公开披露的信息中收集的,如合法的新闻报道、政府信息公开等渠道。
四、我们如何保护您的用户信息
1. 我们已使用符合业界标准的安全防护措施保护您提供的用户信息,防止数据遭到未经授权的访问、公开披露、使用、修改、损坏或丢失。我们会采取一切合理可行的措施,保护您的用户信息。例如,在您的浏览器与“服务”之间交换数据时受 SSL 加密保护;我们同时对网站提供https安全浏览方式;我们会使用加密技术确保数据的保密性;我们会使用受信赖的保护机制防止数据遭到恶意攻击;我们会部署访问控制机制,确保只有授权人员才可访问用户信息;以及我们会举办安全和隐私保护培训课程,加强员工对于保护用户信息重要性的认识。
2. 目前,我们的重要信息系统已通过信息安全等级保护(三级)测评和备案。
3. 我们已建立专门的管理系统、流程和组织,确保信息安全。例如,我们严格限制有信息访问权限的人员,要求这些人员遵循其保密义务,并对此进行审查。
4. 我们会采取一切合理可行的措施,确保未收集无关的用户信息。我们只会在达成本政策所述目的所需的期限内保留您的用户信息,除非需要延长保留期或受到法律的允许。
5. 互联网并非绝对安全的环境,而且电子邮件、即时通讯、及与其他用户的交流方式并未加密,我们强烈建议您不要通过此类方式发送用户信息。
6. 我们将定期更新并公开安全风险、用户信息安全影响评估等报告的有关内容。您可通过以下方式获得:
7. 互联网环境并非百分之百安全,我们将尽力确保您发送给我们的任何信息的安全性。即使我们做出了很大努力,采取了一切合理且必要的措施,仍然有可能无法杜绝您的用户信息被非法访问、被非法盗取,被非法篡改或毁坏,导致您的合法权益受损,请您理解信息网络的上述风险并自愿承担。
8. 在不幸发生用户信息安全事件后,我们将按照法律法规的要求,及时向您告知:安全事件的基本情况和可能的影响、我们已采取或将要采取的处置措施、您可自主防范和降低风险的建议、对您的补救措施等。我们将及时将事件相关情况以邮件、信函、电话、推送通知等方式告知您,难以逐一告知用户信息主体时,我们会采取合理、有效的方式发布公告。同时,我们还将按照监管部门要求,主动上报用户信息安全事件的处置情况。
五、您的权利
按照中国相关的法律、法规、标准,以及其他国家、地区的通行做法,我们保障您对自己的用户信息行使以下权利:
(一)访问您的用户信息
您有权访问您的用户信息,法律法规规定的例外情况除外。如果您想行使数据访问权,可以通过以下方式自行访问:https://ask.dcloud.net.cn/
如果您无法通过上述链接访问这些用户信息,您可以随时使用我们的 Web 表单联系,或发送电子邮件至service@dcloud.io。我们将在30天内回复您的访问请求。
对于您在使用我们的产品或服务过程中产生的其他用户信息,只要我们不需要过多投入,我们会向您提供。如果您想行使数据访问权,请发送电子邮件至service@dcloud.io。
(二)更正您的用户信息
当您发现我们处理的关于您的用户信息有错误时,您有权要求我们作出更正。您可以通过“(一)访问您的用户信息”中罗列的方式提出更正申请。 如果您无法通过上述链接更正这些用户信息,您可以随时使用我们的 Web 表单联系,或发送电子邮件至service@dcloud.io。我们将在30天内回复您的更正请求。
(三)删除您的用户信息
在以下情形中,您可以向我们提出删除用户信息的请求:
1. 如果我们处理用户信息的行为违反法律法规;
2. 如果我们收集、使用您的用户信息,却未征得您的同意;
3. 如果我们处理用户信息的行为违反了与您的约定;
4. 如果您不再使用我们的产品或服务,或您注销了账号;
5. 如果我们不再为您提供产品或服务。
我们将会根据您的删除请求进行评估,若满足相应规定,我们将会采取相应步骤进行处理。当您向我们提出删除请求时,我们可能会要求您进行身份验证,以保障账户的安全。当您从我们的服务中删除信息后,因为适用的法律和安全技术,我们可能不会立即从备份系统中删除相应的信息,我们将安全存储您的信息直到备份可以清除或实现匿名化。
(四)改变您授权同意的范围
每个业务功能需要一些基本的用户信息才能得以完成(见本政策“第一部分”)。对于用户信息的收集和使用,您可以随时给予或收回您的授权同意。您可以通过以下方式自行操作:https://ask.dcloud.net.cn/
当您收回同意后,我们将不再处理相应的用户信息。同时也请您注意,您撤销授权同意可能会导致某些后果,例如我们可能无法继续为您提供相应的服务或特定的功能,但您收回同意的决定,不会影响此前基于您的授权而开展的用户信息处理。
(五)用户信息主体注销账户
您随时可注销此前注册的账户,您可以通过以下方式自行操作:https://ask.dcloud.net.cn/
在注销账户之后,我们将停止为您提供产品或服务并依据您的要求,删除或匿名化您的信息,法律法规另有规定的除外。这也将可能导致您失去对您账户中数据的访问权,请您谨慎操作。
(六)用户信息主体获取用户信息副本
您有权获取您的用户信息副本,您可以通过以下方式自行操作:https://ask.dcloud.net.cn/
在技术可行的前提下,例如数据接口匹配,我们还可按您的要求,直接将您的用户信息副本传输给您指定的第三方。
(七)约束信息系统自动决策
在某些业务功能中,我们可能仅依据信息系统、算法等在内的非人工自动决策机制作出决定。如果这些决定显著影响您的合法权益,您有权要求我们作出解释,我们也将提供适当的救济方式。
(八)响应您的上述请求
为保障安全,您可能需要提供书面请求,或以其他方式证明您的身份。我们可能会先要求您验证自己的身份,然后再处理您的请求。
我们将在三十天内作出答复。如您不满意,还可以通过以下途径投诉:service@dcloud.io
对于您合理的请求,我们原则上不收取费用,但对多次重复、超出合理限度的请求,我们将视情况收取一定成本费用。对于那些无端重复、需要过多技术手段(例如,需要开发新系统或从根本上改变现行惯例)、给他人合法权益带来风险或者非常不切实际的请求,我们可能会予以拒绝。也请您理解,出于安全保障的考虑、相关法律法规的要求或技术上的限制,对于您的某些请求我们可能无法做出响应,例如以下情形:
1. 与用户信息控制者履行法律法规规定的义务相关的;
2. 与国家安全、国防安全直接相关的;
3. 与公共安全、公共卫生、重大公共利益直接相关的;
4. 与犯罪侦查、起诉、审判和执行判决等直接相关的;
5. 用户信息控制者有充分证据表明用户信息主体存在主观恶意或滥用权利的;
6. 出于维护用户信息主体或其他个人的生命、财产等重大合法权益但又很难得到本人同意的;
7. 响应用户信息主体的请求将导致用户信息主体或其他个人、组织的合法权益受到严重损害的;
8. 涉及商业秘密的。
六、我们如何处理儿童的个人信息
我们非常重视儿童个人信息的保护,我们的产品、网站和服务主要面向成人。如果没有父母或监护人的同意,儿童不应创建自己的用户账户。尽管当地法律和习俗对儿童的定义不同,但我们将不满 14 周岁的任何人均视为儿童。
对于经父母或监护人同意而收集儿童用户信息的情况,我们只会在受到法律允许、父母或监护人明确同意或者保护儿童所必要的情况下储存、使用或公开披露此信息,否则我们会设法尽快删除相关数据。
鉴于现有技术和业务模式的限制,我们很难主动识别儿童的个人信息,如果您发现我们在不知情的情况下或在未事先获得可证实的监护人同意的情况下收集了儿童的个人信息,您可以及时联系我们,我们将在发现后设法及时删除,如果我们自己发现前述情形的,我们也会及时删除,法律要求我们保留的除外。
七、您的用户信息如何储存以及如何在全球范围转移
原则上,我们在中华人民共和国境内收集和产生的用户信息,将存储在中华人民共和国境内。我们只会在本政策所述目的和用途所需的期限内和法律法规规定的最短期限内保留您的用户信息,超出上述保留期间后,我们会根据适用法律法规的要求删除您的用户信息或匿名化处理。法律法规另有规定,或出于公共利益、科学历史研究等的目的,或您的另行授权同意的除外,我们可能需要较长时间保留相关数据。
由于我们通过遍布全球的资源和服务器提供产品或服务,这意味着,在获得您的授权同意后,您的用户信息可能会被转移到您使用产品或服务所在国家/地区的境外管辖区,或者受到来自这些管辖区的访问。
此类管辖区可能设有不同的数据保护法,甚至未设立相关法律。在此类情况下,我们会确保您的用户信息得到在中华人民共和国境内足够同等的保护。例如,我们会请求您对跨境转移用户信息的同意,或者在跨境数据转移之前实施数据去标识化等安全举措。
八、本政策如何更新
我们的隐私政策可能变更。未经您明确同意,我们不会削减您按照本隐私政策所应享有的权利。我们会在本页面上发布对本政策所做的任何变更。
对于重大变更,我们还会提供更为显著的通知。本政策所指的重大变更包括但不限于:
1. 我们的服务模式发生重大变化。如处理用户信息的目的、处理的用户信息类型、用户信息的使用方式等;
2. 我们在所有权结构、组织架构等方面发生重大变化。如业务调整、破产并购等引起的所有者变更等;
3. 用户信息共享、转让或公开披露的主要对象发生变化;
4. 您参与用户信息处理方面的权利及其行使方式发生重大变化;
5. 我们负责处理用户信息安全的责任部门、联络方式及投诉渠道发生变化时;
6. 用户信息安全影响评估报告表明存在高风险时。
我们还会将本政策的旧版本存档,供您查阅。
九、如何联系我们
如果您对本隐私政策有任何疑问、意见或建议,通过以下方式与我们联系:
电子邮件:service@dcloud.io
一般情况下,我们将在三十天内回复。
如果您对我们的回复不满意,特别是我们的用户信息处理行为损害了您的合法权益,您还可以通过以下外部途径寻求解决方案:service@dcloud.io

查看文件

@@ -1,5 +0,0 @@
hello uni-app x 是 uni-app x 项目的演示工程。
uni-app x [开发文档](https://doc.dcloud.net.cn/uni-app-x/)
页面以.uvue为扩展名,.test.js为该页面的自动化测试脚本。

查看文件

@@ -1,224 +0,0 @@
export type SafeArea = {
top : number,
right : number,
bottom : number,
left : number,
width : number,
height : number,
}
export const AGREE_PRIVACY = 'UNI-PRIVACY-AGREE'
export type UserInfo = {
nickName: string,
avatarUrl?: string
}
export type GlobalData = {
str: string
num: number
bool: boolean
obj: UTSJSONObject
null: string | null
arr: number[]
set: Set<string>
map: Map<string, any>
fun: () => string
launchOptions: OnLaunchOptions
showOptions: OnShowOptions
}
type State = {
// app global data 兼容 dom2
globalData: GlobalData
lifeCycleNum: number
// 状态栏高度
statusBarHeight: number
safeArea : SafeArea
// 设备像素比
devicePixelRatio : number
// 事件判断回调
eventCallbackNum: number
// 是否匹配左侧窗口
noMatchLeftWindow:boolean
// 当前激活的tab页
active: string
leftWinActive:string
// 是否同意隐私政策
agreeToPrivacy: boolean | null
// 是否同意截屏
allowCapture: boolean,
// 是否暗黑主题(适配web端)
isDarkMode: boolean
// 是否无网环境
netless: boolean,
// 登录用户信息
userInfo: UserInfo | null
}
const getAgreePrivacy = () => {
const data = uni.getStorageSync(AGREE_PRIVACY)
if (typeof data === 'boolean') {
return data
}
if (data == null) {
return null
}
return null
}
export const state = reactive({
globalData: {
str: 'default globalData str',
num: 0,
bool: false,
obj: {
str: 'default globalData obj str',
num: 0,
bool: false,
} as UTSJSONObject,
null: null as string | null,
arr: [] as number[],
set: new Set<string>(),
map: new Map<string, any>(),
fun: (): string => {
return 'globalData fun'
},
launchOptions: {
path: '',
} as OnLaunchOptions,
showOptions: {
path: ''
} as OnShowOptions
} as GlobalData,
lifeCycleNum: 0,
statusBarHeight: 0,
devicePixelRatio: 1,
eventCallbackNum: 0,
noMatchLeftWindow: true,
active: 'componentPage',
leftWinActive: '/pages/component/view/view',
safeArea: {
top: 0,
right: 0,
bottom: 0,
left: 0,
width: 0,
height: 0,
},
agreeToPrivacy: getAgreePrivacy(),
allowCapture: true,
isDarkMode:false,
netless: false,
userInfo: null
} as State)
export const setUserInfo = (userInfo : UserInfo | null) => {
state.userInfo = userInfo
}
export const setLifeCycleNum = (num : number) => {
state.lifeCycleNum = num
}
export const setEventCallbackNum = (num : number) => {
state.eventCallbackNum = num
}
export const setStatusBarHeight = (height : number) => {
state.statusBarHeight = height
}
export const setSafeArea = (value : SafeArea) => {
state.safeArea = value
}
export const setDevicePixelRatio = (devicePixelRatio : number) => {
state.devicePixelRatio = devicePixelRatio
}
export const setMatchLeftWindow = (matchLeftWindow:boolean) => {
state.noMatchLeftWindow = !matchLeftWindow
}
export const setActive = (tabPage:string) => {
state.active = tabPage
}
export const setLeftWinActive = (leftWinActive:string) => {
state.leftWinActive = leftWinActive
}
export const setAgreeToPrivacy = (agree: boolean) => {
state.agreeToPrivacy = agree
uni.setStorageSync(AGREE_PRIVACY, agree);
}
export const setAllowCapture = (allow: boolean) => {
state.allowCapture = allow
}
// 检查系统主题
export const checkSystemTheme = () => {
// #ifdef WEB || MP-WEIXIN
const info = uni.getAppBaseInfo();
state.isDarkMode = info.hostTheme === 'dark';
uni.onHostThemeChange((result) => {
state.isDarkMode = result.hostTheme === 'dark';
});
// #endif
// web和小程序只支持监听宿主的主题,不支持自行设置与宿主不同的主题。但App端支持自行设置主题,因此单独处理
// #ifdef APP
uni.getSystemInfo({
success: (res : GetSystemInfoResult) => {
const appTheme = res.appTheme == "auto" ? res.osTheme! : res.appTheme!
state.isDarkMode = appTheme == 'dark';
}
})
uni.onAppThemeChange((res : AppThemeChangeResult) => {
state.isDarkMode = res.appTheme == 'dark';
})
// #endif
}
export const setNetless = (netless: boolean) => {
state.netless = netless
}
export const updateGlobalData = (key: string, value: any) => {
switch (key) {
case 'str':
state.globalData.str = value as string
break
case 'num':
state.globalData.num = value as number
break
case 'bool':
state.globalData.bool = value as boolean
break
case 'obj':
state.globalData.obj = value as UTSJSONObject
break
case 'null':
state.globalData.null = value as string | null
break
case 'arr':
state.globalData.arr = value as number[]
break
case 'set':
state.globalData.set = value as Set<string>
break
case 'map':
state.globalData.map = value as Map<string, any>
break
case 'fun':
state.globalData.fun = value as () => string
break
case 'launchOptions':
state.globalData.launchOptions = value as OnLaunchOptions
break
case 'showOptions':
state.globalData.showOptions = value! as OnShowOptions
break
}
}

查看文件

@@ -1,8 +0,0 @@
// 仅测试 console.log 时机问题
export class TestConsole {
constructor() {
console.log('test main.uts console.log');
}
}
export const ts = new TestConsole();

查看文件

@@ -1 +0,0 @@
{}

查看文件

@@ -1,52 +0,0 @@
const Sequencer = require("@jest/test-sequencer").default
const startTestFilePaths = [
"pages/API/get-launch-options-sync/get-launch-options-sync.test.js",
"pages/API/get-current-pages/get-current-pages.test.js",
"pages/component/view/view.test.js",
"pages/API/pull-down-refresh/pull-down-refresh.test.js",
"pages/component/global-events/global-events.test.js",
"pages/component/list-view/list-view-refresh.test.js",
"pages/component/scroll-view/scroll-view-refresher.test.js",
"pages/component/global-events/touch-events.test.js",
"pages/component/global-events/touch-events-bubbles.test.js",
"pages/component/global-events/touch-events-case.test.js",
"pages/component/global-events/touch-events-preventDefault.test.js",
"pages/component/swiper/swiper2.test.js",
"pages/component/slider/slider-maxValue.test.js",
"pages/CSS/overflow/overflow-visible-event.test.js",
"pages/API/create-selector-query/create-selector-query-onScroll.test.js",
"pages/component/scroll-view/scroll-view-custom-refresher-props.test.js",
"pages/component/waterflow/waterflow.test.js",
"pages/component/text/text-props.test.js",
"pages/component/rich-text/rich-text-complex.test.js",
"pages/component/web-view/web-view/web-view-local.test.js"
]
const endTestFilePaths = [
"pages/API/navigator/new-page/onLoad.test.js",
"pages/API/modal/modal.test.js",
"pages/API/storage/storage.test.js",
"pages/component/web-view/web-view.test.js"
]
class CustomSequencer extends Sequencer {
sort(tests) {
const startTests = startTestFilePaths
.map((filePath) => {
return tests.find((test) => test.path.endsWith(filePath))
})
.filter(Boolean)
const endTests = endTestFilePaths
.map((filePath) => {
return tests.find((test) => test.path.endsWith(filePath))
})
.filter(Boolean)
const middleTests = tests.filter((test) =>
!startTests.includes(test) && !endTests.includes(test)
);
return [...startTests, ...middleTests, ...endTests]
}
}
module.exports = CustomSequencer

查看文件

@@ -5,7 +5,7 @@ import path from 'node:path';
const MP_ROOT = path.resolve('m:/2026项目/CRMmp/mpuniapp');
// 17 个全量页面路由列表
// 18 个全量页面路由列表
const ALL_PAGES = [
'pages/portal/index.uvue',
'pages/portal/exchange.uvue',
@@ -13,6 +13,7 @@ const ALL_PAGES = [
'pages/portal/article-detail.uvue',
'pages/portal/ai-chat.uvue',
'pages/common/login.uvue',
'pages/owner/hall/index.uvue',
'pages/owner/card/index.uvue',
'pages/owner/card/edit.uvue',
'pages/owner/card/qrcode.uvue',
@@ -35,8 +36,8 @@ const ALL_COMPONENTS = [
// 4 个 TabBar 页面
const TABBAR_PAGES = [
'pages/owner/hall/index.uvue',
'pages/owner/card/index.uvue',
'pages/owner/radar.uvue',
'pages/owner/leads.uvue',
'pages/owner/profile/index.uvue',
];
@@ -67,7 +68,7 @@ const BOTTOM_CTA_PAGES = [
'pages/owner/team/overview.uvue',
];
test('1. 全端 17 个页面与 3 个组件文件均存在', () => {
test('1. 全端页面与 3 个组件文件均存在', () => {
for (const pagePath of ALL_PAGES) {
const fullPath = path.join(MP_ROOT, pagePath);
assert.ok(fs.existsSync(fullPath), `页面必须存在: ${pagePath}`);

查看文件

@@ -1,52 +1,24 @@
{
"light": {
"navigationBarTextStyle": "white",
"navigationBarBackgroundColor": "#007AFF",
"backgroundColorContent": "#efeff4",
"tabBarPagebackgroundColorContent": "#f8f8f8",
"richTextPageBackgroundColorContent": "#ffffff",
"backgroundColor": "#efeff4",
"navigationBarTextStyle": "black",
"navigationBarBackgroundColor": "#FFFFFF",
"backgroundColor": "#F7F8FA",
"backgroundColorContent": "#F7F8FA",
"backgroundTextStyle": "dark",
"tabBarColor": "#7A7E83",
"tabBarSelectedColor": "#007AFF",
"tabBarBorderStyle": "black",
"tabBarBackgroundColor": "#F8F8F8",
"tabBarComponentIconPath": "static/component.png",
"tabBarComponentSelectedIconPath": "static/componentHL.png",
"tabBarAPIIconPath": "static/api.png",
"tabBarAPISelectedIconPath": "static/apiHL.png",
"tabBarCSSIconPath": "static/css.png",
"tabBarCSSSelectedIconPath": "static/cssHL.png",
"tabBarTemplateIconPath": "static/template.png",
"tabBarTemplateSelectedIconPath": "static/templateHL.png",
"tabBarUniUIIconPath": "static/extui.png",
"tabBarUniUISelectedIconPath": "static/extuiHL.png"
"tabBarColor": "#667085",
"tabBarSelectedColor": "#0B5CFF",
"tabBarBorderStyle": "white",
"tabBarBackgroundColor": "#FFFFFF"
},
"dark": {
"navigationBarTextStyle": "white",
"navigationBarBackgroundColor": "#1F1F1F",
"backgroundColor": "#1F1F1F",
"backgroundColorContent": "#646464",
"tabBarPagebackgroundColorContent": "#1a1a1a",
"richTextPageBackgroundColorContent": "#000000",
"navigationBarBackgroundColor": "#0B0D10",
"backgroundColor": "#0B0D10",
"backgroundColorContent": "#0B0D10",
"backgroundTextStyle": "light",
"tabBarColor": "#cacaca",
"tabBarSelectedColor": "#007AFF",
"tabBarColor": "#98A2B3",
"tabBarSelectedColor": "#4D8DFF",
"tabBarBorderStyle": "white",
"tabBarBackgroundColor": "#1F1F1F",
"tabBarComponentIconPath": "static/component.png",
"tabBarComponentSelectedIconPath": "static/componentHL.png",
"tabBarAPIIconPath": "static/api.png",
"tabBarAPISelectedIconPath": "static/apiHL.png",
"tabBarCSSIconPath": "static/css.png",
"tabBarCSSSelectedIconPath": "static/cssHL.png",
"tabBarTemplateIconPath": "static/template.png",
"tabBarTemplateSelectedIconPath": "static/templateHL.png",
"tabBarUniUIIconPath": "static/extui.png",
"tabBarUniUISelectedIconPath": "static/extuiHL.png"
"tabBarBackgroundColor": "#12151A"
}
}

查看文件

@@ -1 +0,0 @@
欢迎到需求墙投票:[https://vote.dcloud.net.cn/#/?name=uni-app%20x](https://vote.dcloud.net.cn/#/?name=uni-app%20x)

查看文件

@@ -1,76 +1,17 @@
/**
* 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
* 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
*
*/
$uni-color-primary: #0B5CFF;
$uni-color-success: #12B76A;
$uni-color-warning: #F79009;
$uni-color-error: #F04438;
/**
* 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
$uni-text-color: #101828;
$uni-text-color-inverse: #FFFFFF;
$uni-text-color-grey: #667085;
$uni-text-color-placeholder: #98A2B3;
$uni-text-color-disable: #98A2B3;
/* 颜色变量 */
$uni-bg-color: #FFFFFF;
$uni-bg-color-grey: #F7F8FA;
$uni-bg-color-hover: #F2F4F7;
$uni-bg-color-mask: rgba(16, 24, 40, 0.4);
/* 行为相关颜色 */
$uni-color-primary: #007aff;
$uni-color-success: #4cd964;
$uni-color-warning: #f0ad4e;
$uni-color-error: #dd524d;
/* 文字基本颜色 */
$uni-text-color:#333;//基本色
$uni-text-color-inverse:#fff;//反色
$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息
$uni-text-color-placeholder: #808080;
$uni-text-color-disable:#c0c0c0;
/* 背景颜色 */
$uni-bg-color:#ffffff;
$uni-bg-color-grey:#f8f8f8;
$uni-bg-color-hover:#f1f1f1;//点击状态颜色
$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色
/* 边框颜色 */
$uni-border-color:#c8c7cc;
/* 尺寸变量 */
/* 文字尺寸 */
$uni-font-size-sm:12px;
$uni-font-size-base:14px;
$uni-font-size-lg:16;
/* 图片尺寸 */
$uni-img-size-sm:20px;
$uni-img-size-base:26px;
$uni-img-size-lg:40px;
/* Border Radius */
$uni-border-radius-sm: 2px;
$uni-border-radius-base: 3px;
$uni-border-radius-lg: 6px;
$uni-border-radius-circle: 50%;
/* 水平间距 */
$uni-spacing-row-sm: 5px;
$uni-spacing-row-base: 10px;
$uni-spacing-row-lg: 15px;
/* 垂直间距 */
$uni-spacing-col-sm: 4px;
$uni-spacing-col-base: 8px;
$uni-spacing-col-lg: 12px;
/* 透明度 */
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
/* 文章场景相关 */
$uni-color-title: #2C405A; // 文章标题颜色
$uni-font-size-title:20px;
$uni-color-subtitle: #555555; // 二级标题颜色
$uni-font-size-subtitle:26px;
$uni-color-paragraph: #3F536E; // 文章段落颜色
$uni-font-size-paragraph:15px;
$uni-border-color: #E3E7ED;

查看文件

@@ -1,16 +0,0 @@
'use strict';
exports.main = async (event, context) => {
//event为客户端上传的参数
console.log('event : ', event)
//返回数据给客户端
return {
event,
context,
showMessage: 'Hello uniCloud function',
detail: {
functionName: 'echo-cf',
timestamp: Date.now()
}
}
};

查看文件

@@ -1,9 +0,0 @@
{
"name": "echo-cf",
"dependencies": {},
"extensions": {},
"origin-plugin-dev-name": "hello-uniapp-x",
"origin-plugin-version": "1.0.44",
"plugin-dev-name": "hello-uniapp-x",
"plugin-version": "1.0.44"
}

查看文件

@@ -1,22 +0,0 @@
// 云对象教程: https://uniapp.dcloud.net.cn/uniCloud/cloud-obj
// jsdoc语法提示教程:https://ask.dcloud.net.cn/docs/#//ask.dcloud.net.cn/article/129
module.exports = {
_before() {
const clientInfo = this.getClientInfo()
const secretType = clientInfo.secretType
const methodName = this.getMethodName()
const clientInfos = uniCloud.getClientInfos()
// methodName 是客户端调用的方法名
// secretType 是客户端调用 uniCloud.importObject 传递的参数 secretMethods
if (methodName === 'secureTest' && secretType !== 'both') {
const error = new Error('客户端指定的secretType不为both,拒绝此次请求')
error.code = 'INVALID_SECRET_TYPE'
throw error // 拒绝本次请求
}
},
secureTest() {
return {
returnBy: "encryption-object.secureTest"
}
}
}

查看文件

@@ -1,11 +0,0 @@
{
"name": "encryption-object",
"dependencies": {},
"extensions": {
"uni-cloud-jql": {}
},
"origin-plugin-dev-name": "hello-uniapp-x",
"origin-plugin-version": "1.0.44",
"plugin-dev-name": "hello-uniapp-x",
"plugin-version": "1.0.44"
}

查看文件

@@ -1,13 +0,0 @@
'use strict';
exports.main = async (event, context) => {
const secretType = context.secretType
// secretType 是客户端调用 uniCloud.callFunction 传递的参数 secretType
if (secretType !== 'both') {
const error = new Error('客户端指定的secretType不为both,拒绝此次请求')
error.code = 'INVALID_SECRET_TYPE'
throw error // 拒绝本次请求
}
return {
returnBy: 'encyption'
}
};

查看文件

@@ -1,11 +0,0 @@
{
"name": "encryption",
"dependencies": {},
"extensions": {
"uni-cloud-jql": {}
},
"origin-plugin-dev-name": "hello-uniapp-x",
"origin-plugin-version": "1.0.44",
"plugin-dev-name": "hello-uniapp-x",
"plugin-version": "1.0.44"
}

查看文件

@@ -1,26 +0,0 @@
// 注意本文件为实人认证API示例代码,如需在业务中使用需补充业务逻辑,比如用户需要登录才能使用实人认证、限制每个用户每天能实人认证的次数等
module.exports = {
_before() {
this.frvManager = uniCloud.getFacialRecognitionVerifyManager({
requestId: this.getUniCloudRequestId()
})
},
async getCertifyId({
realName,
idCard,
metaInfo
} = {}) {
const result = await this.frvManager.getCertifyId({
realName,
idCard,
metaInfo
})
return result
},
async getAuthResult(certifyId) {
const result = await this.frvManager.getAuthResult({
certifyId
})
return result
}
}

查看文件

@@ -1,11 +0,0 @@
{
"name": "facial-recognition-co",
"dependencies": {},
"extensions": {
"uni-cloud-verify": {}
},
"origin-plugin-dev-name": "hello-uniapp-x",
"origin-plugin-version": "1.0.44",
"plugin-dev-name": "hello-uniapp-x",
"plugin-version": "1.0.44"
}

查看文件

@@ -1,15 +0,0 @@
'use strict';
exports.main = async (event, context) => {
const sseChannelObj = event.sseChannel
const sseChannel = uniCloud.deserializeSSEChannel(sseChannelObj)
return new Promise(async (resolve, reject) => {
await sseChannel.write('message1')
await sseChannel.write('message2')
setTimeout(async () => {
await sseChannel.end()
resolve({
errCode: 0
})
}, 300)
})
};

查看文件

@@ -1,12 +0,0 @@
{
"name": "sse",
"dependencies": {},
"extensions": {
"uni-cloud-jql": {},
"uni-cloud-push": {}
},
"origin-plugin-dev-name": "hello-uniapp-x",
"origin-plugin-version": "1.0.44",
"plugin-dev-name": "hello-uniapp-x",
"plugin-version": "1.0.44"
}

查看文件

@@ -1,50 +0,0 @@
// 云对象教程: https://uniapp.dcloud.net.cn/uniCloud/cloud-obj
// jsdoc语法提示教程:https://ask.dcloud.net.cn/docs/#//ask.dcloud.net.cn/article/129
module.exports = {
async add(title, content) {
console.log(title, content)
return {
title,
content,
showMessage: `Todo added, title: ${title}, content: ${content}`
}
},
async randomFail() {
const random = Math.random()
console.log(random);
if (random > 0.5) {
return {
errSubject: 'randomFail',
errCode: 'RANDOM_FAIL',
errMsg: '[预期内的报错]执行失败请重试'
}
} else {
return {
errCode: 0,
errMsg: '',
showMessage: '执行成功'
}
}
},
async fail() {
return {
errSubject: 'fail',
errCode: 'TEST_ERROR_CODE',
errMsg: '[预期内的报错]执行失败',
tips: 'DO_NOT_TRY_AGAIN'
}
},
async failWithNumberErrCode() {
return {
errCode: -1,
errMsg: '[预期内的报错]执行失败',
}
},
async success() {
return {
errCode: 0,
errMsg: '',
showMessage: '执行成功'
}
}
}

查看文件

@@ -1,9 +0,0 @@
{
"name": "todo",
"dependencies": {},
"extensions": {},
"origin-plugin-dev-name": "hello-uniapp-x",
"origin-plugin-version": "1.0.44",
"plugin-dev-name": "hello-uniapp-x",
"plugin-version": "1.0.44"
}

查看文件

@@ -1,87 +0,0 @@
const configCenter = require("uni-config-center");
const config = configCenter({ pluginId: 'uni-login-test-co' }).requireFile('config.js');
module.exports = {
_before: function() {
},
/**
* App微信登录并返回openid
* @param {string} code
* @param {string} appid
*/
async loginByWeixin(data = {}) {
let {
code,
appid
} = data;
if (!code) {
return { errCode: -1, errMsg: 'code参数不能为空' }
}
if (!appid) {
return { errCode: -1, errMsg: 'appid参数不能为空' }
}
const secret = config.weixin[appid];
if (!secret) {
return { errCode: -1, errMsg: `appid:${appid} 未找到对应的 secret 配置` }
}
const url = `https://api.weixin.qq.com/sns/oauth2/access_token`;
const res = await uniCloud.httpclient.request(url, {
method: 'GET',
data: {
appid,
secret,
code,
grant_type: "authorization_code"
},
contentType: 'json',
dataType: 'json'
});
/* const res = {
"data": {
"access_token": "101_tmi6EalA41uRzkP8DScXi7BxXjRuuL_aHB8VGrMb7Ng6s1rmzWVV8dKCUXYTjpZMumRLChp_sJ-vGlH5GNwyapOWG3aXvRnPvDxW9lc0A5w",
"expires_in": 7200,
"refresh_token": "101_IxOQj08nKiwA86hqVJ7fIlwXabLyNwmUyOWw_ycfKt4bbfo46rjsul7CNEH5_SWS-SRt54Cj-MTZJ3pe7Dh3pgi7pR7SzaJkRPw47H8y666",
"openid": "opjMn6YlpHJa1ZzVyAvMB5O7SabI",
"scope": "snsapi_userinfo",
"unionid": "oU5YytxRnXXHJlXKLQtIuJMcv9sc"
},
"status": 200,
"headers": { "connection": "keep-alive", "content-type": "text/plain", "date": "Tue, 10 Mar 2026 08:42:51 GMT", "content-length": "385" }
} */
const wxRes = res.data;
if (wxRes.errcode) {
return { errCode: wxRes.errcode, errMsg: wxRes.errmsg }
}
const {
access_token,
openid,
unionid
} = res.data;
const userInfoRes = await uniCloud.httpclient.request(`https://api.weixin.qq.com/sns/userinfo?access_token=${access_token}&openid=${openid}$lang=zh-CN`, {
method: 'GET',
contentType: 'json',
dataType: 'json'
});
const {
nickname,
headimgurl, // 头像不一定可以拿到
} = userInfoRes.data;
return {
errCode: 0,
openid,
unionid,
nickname,
headimgurl
}
}
}

查看文件

@@ -1,11 +0,0 @@
{
"name": "uni-login-test-co",
"dependencies": {
"uni-config-center": "file:../../../uni_modules/uni-config-center/uniCloud/cloudfunctions/common/uni-config-center"
},
"extensions": {},
"origin-plugin-dev-name": "hello-uniapp-x",
"origin-plugin-version": "1.0.44",
"plugin-dev-name": "hello-uniapp-x",
"plugin-version": "1.0.44"
}

查看文件

@@ -1,4 +0,0 @@
loginByWeixin({
"code": "011rKyll2wnDjh4Ns3ml2yXW0e3rKylu",
"appid": "wxd569c7238830733b"
})

查看文件

@@ -1,21 +0,0 @@
module.exports = {
async sendPushMessage(cid) {
// 防止非法调用
if (!cid) {
throw new Error('请传入推送客户端ID')
}
if (typeof cid !== 'string') {
throw new Error('推送客户端ID必须为字符串')
}
const pushManager = uniCloud.getPushManager({appId: '__UNI__HelloUniAppX'})
const res = await pushManager.sendMessage({
push_clientid: cid,
title: '测试推送标题',
content: '测试推送内容',
payload: {
data: '测试推送数据'
}
})
return res
}
}

查看文件

@@ -1,12 +0,0 @@
{
"name": "uni-push-co",
"dependencies": {},
"extensions": {
"uni-cloud-jql": {},
"uni-cloud-push": {}
},
"origin-plugin-dev-name": "hello-uniapp-x",
"origin-plugin-version": "1.0.44",
"plugin-dev-name": "hello-uniapp-x",
"plugin-version": "1.0.44"
}

查看文件

@@ -1,15 +0,0 @@
'use strict';
exports.main = async function(event, context) {
const res = await uniCloud.getPhoneNumber({
appid: context.appId, // 替换成自己开通一键登录的应用的DCloud appid
provider: 'univerify',
access_token: event.access_token,
openid: event.openid
})
// 执行入库等操作,正常情况下不要把完整手机号返回给前端
return {
errCode: 0,
errMsg: '获取手机号成功',
res: res
}
}

查看文件

@@ -1,11 +0,0 @@
{
"name": "univerify",
"dependencies": {},
"extensions": {
"uni-cloud-verify": {}
},
"origin-plugin-dev-name": "hello-uniapp-x",
"origin-plugin-version": "1.0.44",
"plugin-dev-name": "hello-uniapp-x",
"plugin-version": "1.0.44"
}

查看文件

@@ -1,6 +0,0 @@
'use strict';
exports.main = async (event, context) => {
return {
returnBy: 'verify-client'
}
};

查看文件

@@ -1,11 +0,0 @@
{
"name": "verify-client",
"dependencies": {},
"extensions": {
"uni-cloud-jql": {}
},
"origin-plugin-dev-name": "hello-uniapp-x",
"origin-plugin-version": "1.0.44",
"plugin-dev-name": "hello-uniapp-x",
"plugin-version": "1.0.44"
}

查看文件

@@ -1,12 +0,0 @@
// 本文件用于,使用JQL语法操作项目关联的uniCloud空间的数据库,方便开发调试和远程数据库管理
// 编写clientDB的js API(也支持常规js语法,比如var),可以对云数据库进行增删改查操作。不支持uniCloud-db组件写法
// 可以全部运行,也可以选中部分代码运行。点击工具栏上的运行按钮或者按下【F5】键运行代码
// 如果文档中存在多条JQL语句,只有最后一条语句生效
// 如果混写了普通js,最后一条语句需是数据库操作语句
// 此处代码运行不受DB Schema的权限控制,移植代码到实际业务中注意在schema中配好permission
// 不支持clientDB的action
// 数据库查询有最大返回条数限制,详见:https://uniapp.dcloud.net.cn/uniCloud/cf-database.html#limit
// 详细JQL语法,请参考:https://uniapp.dcloud.net.cn/uniCloud/jql.html
// 下面示例查询uni-id-users表的所有数据
db.collection('uni-id-users').get();

查看文件

@@ -1,25 +0,0 @@
// 文档教程: https://uniapp.dcloud.net.cn/uniCloud/schema
{
"bsonType": "object",
"required": [],
"permission": {
"read": true,
"create": true,
"update": true,
"delete": true
},
"properties": {
"_id": {
"description": "ID,系统自动生成"
},
"id": {
"bsonType": "string"
},
"name": {
"bsonType": "string"
},
"tag": {
"bsonType": "string"
}
}
}

查看文件

@@ -1,28 +0,0 @@
// 文档教程: https://uniapp.dcloud.net.cn/uniCloud/schema
{
"bsonType": "object",
"required": [],
"permission": {
"read": true,
"create": true,
"update": true,
"delete": true
},
"properties": {
"_id": {
"description": "ID,系统自动生成"
},
"id": {
"bsonType": "string"
},
"name": {
"bsonType": "string"
},
"tag": {
"bsonType": "string"
},
"foreign_id": {
"foreignKey": "foreign.id"
}
}
}

查看文件

@@ -1,76 +0,0 @@
{
"bsonType": "object",
"permission": {
"read": true,
"create": true,
"update": true,
"delete": true
},
"required": ["username", "mobile"],
"properties": {
"_id": {
"description": "存储文档 ID(用户 ID),系统自动生成"
},
"username": {
"bsonType": "string",
"title": "姓名",
"description": "姓名",
"order": 1,
"trim": "both"
},
"gender": {
"bsonType": "int",
"title": "性别",
"description": "用户性别:0 未知 1 男性 2 女性",
"order": 2,
"defaultValue": 0,
"enum": [{
"text": "未知",
"value": 0
},
{
"text": "男",
"value": 1
},
{
"text": "女",
"value": 2
}
]
},
"mobile": {
"bsonType": "string",
"title": "电话",
"order": 3,
"description": "电话",
"pattern": "^\\+?[0-9-]{3,20}$",
"trim": "both"
},
"email": {
"bsonType": "string",
"format": "email",
"title": "邮箱",
"order": 4,
"description": "邮箱地址",
"trim": "both"
},
"comment": {
"bsonType": "string",
"title": "备注",
"order": 5,
"description": "备注",
"trim": "both",
"component": {
"name": "textarea"
}
},
"create_date": {
"bsonType": "timestamp",
"description": "创建时间",
"forceDefaultValue": {
"$env": "now"
}
}
},
"version": "0.0.1"
}

查看文件

@@ -1,30 +0,0 @@
// 文档教程: https://uniapp.dcloud.net.cn/uniCloud/schema
{
"bsonType": "object",
"required": [],
"permission": {
"read": true,
"create": true,
"update": true,
"delete": true
},
"properties": {
"_id": {
"description": "ID,系统自动生成"
},
"num": {
"bsonType": "int"
},
"tag": {
"bsonType": "string",
"maxLength": 50,
"minLength": 0
},
"date": {
"bsonType": "date"
},
"point": {
"bsonType": "object"
}
}
}

查看文件

@@ -1,40 +0,0 @@
{
"bsonType": "object",
"permission": {
"read": true,
"create": true,
"update": true,
"delete": true
},
"required": ["title", "comment"],
"properties": {
"_id": {
"description": "存储文档 ID(用户 ID),系统自动生成"
},
"title": {
"bsonType": "string",
"title": "姓名",
"description": "姓名",
"order": 1,
"trim": "both"
},
"comment": {
"bsonType": "string",
"title": "备注",
"order": 5,
"description": "备注",
"trim": "both",
"component": {
"name": "textarea"
}
},
"create_date": {
"bsonType": "timestamp",
"description": "创建时间",
"forceDefaultValue": {
"$env": "now"
}
}
},
"version": "0.0.1"
}

查看文件

@@ -1,134 +0,0 @@
<template>
<native-view @init="onInit"></native-view>
</template>
<script setup>
import {
CalendarPicker
} from '@/uni_modules/CalendarPicker'
const props = defineProps < {
/**
* 设置选中项的日期。选中的日期未设置或日期格式不符合规范则为默认值。
* 默认值:当前系统日期。
*/
selected ? : Date
/**
* 描述日期选中态底板样式。
* 默认值:底板样式为圆形。
* 说明:hintRadius为0,底板样式为直角矩形。hintRadius为0 ~ 16,底板样式为圆角矩形。hintRadius>=16,底板样式为圆形
*/
hintRadius ? : number
/**
* 设置选择器与入口组件的对齐方式。
* 默认end
* start: 左对齐
* center: 居中对齐
* end: 右对齐
*/
alignType ? : 'start' | 'center' | 'end'
/**
* 按照对齐类型对齐后,选择器相对入口组件的水平方向偏移量
* 默认:0
*/
offsetX ? : number | string
/**
* 按照对齐类型对齐后,选择器相对入口组件的竖直方向偏移量
* 默认:0
*/
offsetY ? : number | string
/**
* 文本颜色
*/
color ? : string
/**
* 文本尺寸
*/
fontSize ? : number
/**
* 字体粗细
* Lighter: 100
* Normal: 400
* Regular: 400
* Medium: 500
* Bold: 700
* Bolder: 900
*/
fontWeight ? : 100 | 400 | 500 | 700 | 900
/**
* 字体样式
* normal: 标准的字体样式
* italic: 斜体的字体样式
*/
fontStyle ? : 'normal' | 'italic'
} >
()
const emit = defineEmits < {
change: [date: Date]
} > ()
const picker: CalendarPicker = new CalendarPicker()
picker.onChange((value: Date) => {
emit('change', value)
})
watchEffect(() => {
const selected = props.selected
if (selected != null) {
picker.setSelected(selected)
}
})
watchEffect(() => {
const alignType = props.alignType
if (alignType != null) {
picker.setAlignType(alignType)
}
})
watchEffect(() => {
const offsetX = props.offsetX
if (offsetX != null) {
picker.setOffsetX(offsetX)
}
})
watchEffect(() => {
const offsetY = props.offsetY
if (offsetY != null) {
picker.setOffsetY(offsetY)
}
})
watchEffect(() => {
const hintRadius = props.hintRadius
if (hintRadius != null) {
picker.setHintRadius(hintRadius)
}
})
watchEffect(() => {
const color = props.color
if (color != null) {
picker.setColor(color)
}
})
watchEffect(() => {
const fontSize = props.fontSize
if (fontSize != null) {
picker.setFontSize(fontSize)
}
})
watchEffect(() => {
const fontStyle = props.fontStyle
if (fontStyle != null) {
picker.setFontStyle(fontStyle)
}
})
watchEffect(() => {
const fontWeight = props.fontWeight
if (fontWeight != null) {
picker.setFontWeight(fontWeight)
}
})
function onInit(event: UniNativeViewInitEvent) {
picker.init(event.detail.element)
}
</script>

查看文件

@@ -1,83 +0,0 @@
{
"id": "CalendarPicker",
"displayName": "CalendarPicker",
"version": "1.0.0",
"description": "CalendarPicker",
"keywords": [
"CalendarPicker"
],
"repository": "",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"type": "component-vue",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "",
"data": "",
"permissions": ""
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "u",
"aliyun": "u",
"alipay": "u"
},
"client": {
"Vue": {
"vue2": "u",
"vue3": "u"
},
"App": {
"app-vue": "u",
"app-nvue": "u",
"app-uvue": "u"
},
"H5-mobile": {
"Safari": "u",
"Android Browser": "u",
"微信浏览器(Android)": "u",
"QQ浏览器(Android)": "u"
},
"H5-pc": {
"Chrome": "u",
"IE": "u",
"Edge": "u",
"Firefox": "u",
"Safari": "u"
},
"小程序": {
"微信": "u",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}

查看文件

@@ -1 +0,0 @@
# test-native-view

查看文件

@@ -1,13 +0,0 @@
@Builder
export function buildCalendarPicker(params: ESObject) {
CalendarPicker({ hintRadius: params.hintRadius, selected: params.selected })
.edgeAlign(params.alignType, { dx: params.offsetX, dy: params.offsetY })
.textStyle({
color: params.color,
font: { size: params.fontSize, weight: params.fontWeight, style: params.fontStyle }
})
.onChange((value) => {
params.onChange(value)
})
.attributeModifier(params.attributeUpdater)
}

查看文件

@@ -1,108 +0,0 @@
import { BuilderNode } from "@kit.ArkUI"
import { buildCalendarPicker } from "./builder.ets"
interface CalendarPickerOptions {
selected ?: Date
hintRadius ?: number
alignType ?: CalendarAlign
offsetX ?: number | string
offsetY ?: number | string
color ?: string
fontSize ?: number
fontWeight ?: 100 | 400 | 500 | 700 | 900
fontStyle ?: FontStyle
onChange ?: (value : Date) => void
}
export class CalendarPicker {
private onChanges : Array<(value : Date) => void> = []
private params : CalendarPickerOptions = {
onChange: (value : Date) => {
this.onChanges.forEach((onChange) => {
onChange(value)
})
}
}
private builder : BuilderNode<[CalendarPickerOptions]> | null = null
init(element : UniNativeViewElement) {
this.builder =
element.bindHarmonyWrappedBuilder(wrapBuilder<[CalendarPickerOptions]>(buildCalendarPicker), this.params)
}
onChange(callback : (value : Date) => void) : CalendarPicker {
this.onChanges.push(callback)
return this
}
setSelected(date : Date) : CalendarPicker {
this.params.selected = date
this.builder?.update(this.params)
return this
}
setAlignType(alignType : 'start' | 'center' | 'end') : CalendarPicker {
switch (alignType) {
case 'start':
this.params.alignType = CalendarAlign.START
break
case 'center':
this.params.alignType = CalendarAlign.CENTER
break
case 'end':
this.params.alignType = CalendarAlign.END
break
}
this.builder?.update(this.params)
return this
}
setOffsetX(offsetX : number | string) : CalendarPicker {
this.params.offsetX = offsetX
this.builder?.update(this.params)
return this
}
setOffsetY(offsetY : number | string) : CalendarPicker {
this.params.offsetY = offsetY
this.builder?.update(this.params)
return this
}
setHintRadius(hintRadius : number) : CalendarPicker {
this.params.hintRadius = hintRadius
this.builder?.update(this.params)
return this
}
setColor(color : string) : CalendarPicker {
this.params.color = color
this.builder?.update(this.params)
return this
}
setFontSize(fontSize : number) : CalendarPicker {
this.params.fontSize = fontSize
this.builder?.update(this.params)
return this
}
setFontWeight(fontWeight : 100 | 400 | 500 | 700 | 900) : CalendarPicker {
this.params.fontWeight = fontWeight
this.builder?.update(this.params)
return this
}
setFontStyle(fontStyle : 'normal' | 'italic') : CalendarPicker {
switch (fontStyle) {
case 'normal':
this.params.fontStyle = FontStyle.Normal
break
case 'italic':
this.params.fontStyle = FontStyle.Italic
break
}
this.builder?.update(this.params)
return this
}
}

查看文件

@@ -1,56 +0,0 @@
<template>
<!--建议只存在一个根节点,如果native-view存在兄弟节点则需要将其包裹处理-->
<view>
<native-view style="height: 100px;" @init="onviewinit" @customClick="ontap"></native-view>
<button style="width: 50%;margin: 10px 0px;align-self: center;">测试按钮</button>
</view>
</template>
<script setup lang="uts">
import { NativeButton } from "@/uni_modules/native-button";
let button : NativeButton | null = null
//声明属性
const props = defineProps<{ text : string }>()
//声明事件
const emit = defineEmits<{
(e : "load") : void
(e : "buttonTap", event : UniNativeViewEvent) : void
}>()
//声明方法
function updateText(value : string) {
button?.updateText(value)
}
//监听属性变化
watchEffect(() => {
const text = props.text
updateText(text)
})
/**
* native-view组件初始化时触发
* 用于绑定App平台原生View
*/
function onviewinit(e : UniNativeViewInitEvent) {
//获取UniNativeViewElement 传递给NativeButton对象
button = new NativeButton(e.detail.element);
updateText(props.text)
emit("load")
}
/**
* 将native-view组件的ontap事件转换为buttonTap事件
*/
function ontap(e : UniNativeViewEvent) {
emit("buttonTap", e)
}
/**
* App平台需要主动释放资源
*/
onUnmounted(() => {
button?.destroy()
})
</script>

查看文件

@@ -1,82 +0,0 @@
{
"id": "native-button",
"displayName": "native-button",
"version": "0.0.1",
"description": "vue开发button原生组件",
"repository": "",
"engines": {
"HBuilderX": "^4.25"
},
"dcloudext": {
"type": "uts",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "插件不采集任何数据",
"permissions": "无"
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"Vue": {
"vue2": "n",
"vue3": "y"
},
"App": {
"app-android": {
"minVersion": "21"
},
"app-ios": {
"minVersion": "9"
}
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "u",
"QQ": "y",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}

查看文件

@@ -1,3 +0,0 @@
## native-button
通过 object 封装原生平台Button按钮

查看文件

@@ -1,3 +0,0 @@
{
"minSdkVersion": "21"
}

某些文件未显示,因为此 diff 中更改的文件太多 显示更多