小程序初始提交

This commit is contained in:
jdc
2025-11-13 10:36:23 +08:00
parent f26b4f9a2f
commit 5db3b180eb
447 changed files with 83351 additions and 0 deletions

2
.idea/vcs.xml generated
View File

@@ -2,6 +2,8 @@
<project version="4"> <project version="4">
<component name="VcsDirectoryMappings"> <component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" /> <mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$/cool-admin-java" vcs="Git" />
<mapping directory="$PROJECT_DIR$/cool-admin-vue" vcs="Git" /> <mapping directory="$PROJECT_DIR$/cool-admin-vue" vcs="Git" />
<mapping directory="$PROJECT_DIR$/cool-unix" vcs="Git" />
</component> </component>
</project> </project>

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,349 @@
const fs = require("fs");
const path = require("path");
const AdmZip = require("adm-zip");
// 清理所有临时文件
function cleanupTempDir() {
const tempDir = path.join(".cool", "temp");
if (fs.existsSync(tempDir)) {
try {
fs.rmSync(tempDir, { recursive: true, force: true });
} catch (error) {
console.warn(`❌ 清理临时目录失败: ${ tempDir }`, error);
}
}
}
// 确保临时目录存在
function ensureTempDir() {
const tempDir = path.join(".cool", "temp");
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
}
// 创建icons目录和子目录
function ensureDistDir(folderName = "") {
const iconsPath = folderName ? path.join("icons", folderName) : "icons";
if (!fs.existsSync(iconsPath)) {
fs.mkdirSync(iconsPath, { recursive: true });
}
}
// 读取zip文件列表
function getZipFiles() {
const iconsDir = path.join(".cool", "icons");
if (!fs.existsSync(iconsDir)) {
console.error(`❌ 目录不存在: ${ iconsDir }`);
return [];
}
return fs.readdirSync(iconsDir).filter((item) => {
const filePath = path.join(iconsDir, item);
const stats = fs.statSync(filePath);
return stats.isFile() && item.endsWith(".zip");
});
}
// 解压zip文件到临时目录
function extractZipFile(zipPath, folderName) {
try {
const zip = new AdmZip(zipPath);
const tempDir = path.join(".cool", "temp", folderName);
// 确保临时目录存在
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
// 解压到临时目录
zip.extractAllTo(tempDir, true);
// 检查是否有额外的顶层文件夹
const extractedItems = fs.readdirSync(tempDir);
// 如果只有一个项目且是文件夹,则可能是额外的包装文件夹
if (extractedItems.length === 1) {
const singleItem = extractedItems[0];
const singleItemPath = path.join(tempDir, singleItem);
const stats = fs.statSync(singleItemPath);
if (stats.isDirectory()) {
// 检查这个文件夹是否包含我们需要的文件
const innerItems = fs.readdirSync(singleItemPath);
const hasIconFiles = innerItems.some(
(item) =>
item.endsWith(".ttf") || item.endsWith(".json") || item.endsWith(".css")
);
if (hasIconFiles) {
return singleItemPath;
}
}
}
return tempDir;
} catch (error) {
console.error(`❌ 解压失败: ${ zipPath }`, error);
return null;
}
}
// 将TTF文件转换为base64
function ttfToBase64(ttfPath) {
try {
const ttfBuffer = fs.readFileSync(ttfPath);
return ttfBuffer.toString("base64");
} catch (error) {
console.error(`❌ 读取TTF文件失败: ${ ttfPath }`, error);
return null;
}
}
// 生成TypeScript文件
function generateTypeScript(originalFolderName, camelCaseName, iconData, iconPrefix) {
const tsContent = `export const ${ camelCaseName } = {\n${ iconData
.map((item) => `\t"${ iconPrefix }${ item.name }": "${ item.unicode }"`)
.join(",\n") }\n};\n`;
const outputPath = path.join("icons", originalFolderName, "index.ts");
fs.writeFileSync(outputPath, tsContent);
}
// 生成SCSS文件
function generateSCSS(originalFolderName, base64Data) {
const scssContent = `@font-face {\n\tfont-family: "${ toCamelCase(originalFolderName) }";\n\tsrc: url("data:font/ttf;base64,${ base64Data }") format("woff");\n}\n`;
const outputPath = path.join("icons", originalFolderName, "index.scss");
fs.writeFileSync(outputPath, scssContent);
}
// 从CSS文件提取图标数据用于remixicon等
function extractIconsFromCSS(cssPath) {
try {
const cssContent = fs.readFileSync(cssPath, "utf8");
const iconData = [];
// 匹配CSS中的图标规则例如.ri-home-line:before { content: "\ee2b"; }
const regex = /\.ri-([^:]+):before\s*{\s*content:\s*"\\([^"]+)"/g;
let match;
while ((match = regex.exec(cssContent)) !== null) {
const iconName = match[1];
const unicode = match[2];
iconData.push({
name: iconName,
unicode: unicode
});
}
return iconData;
} catch (error) {
console.error(`❌ 读取CSS文件失败: ${ cssPath }`, error);
return [];
}
}
// 读取和处理图标数据
function processIconData(jsonPath) {
try {
const jsonData = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
return jsonData.glyphs.map((item) => ({
name: item.font_class,
unicode: item.unicode
}));
} catch (error) {
console.error(`❌ 读取JSON文件失败: ${ jsonPath }`, error);
return [];
}
}
// 读取iconfont图标前缀
function getIconPrefix(jsonPath) {
try {
const jsonData = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
return jsonData.css_prefix_text;
} catch (error) {
console.error(`❌ 读取JSON文件失败: ${jsonPath}`, error);
return [];
}
}
// 将连字符转换为驼峰命名的函数
function toCamelCase(str) {
return str.replace(/-([a-z])/g, (match, letter) => letter.toUpperCase());
}
// 处理单个zip文件
function processZipFile(zipFileName) {
const originalFolderName = path.basename(zipFileName, ".zip");
const folderName = toCamelCase(originalFolderName); // 转换为驼峰命名用于变量名
const zipPath = path.join(".cool", "icons", zipFileName);
// 解压zip文件 (使用原始文件夹名称)
const tempDir = extractZipFile(zipPath, originalFolderName);
if (!tempDir) {
return null;
}
// 图标库名称
const ptName = ["iconfont", "remixicon"];
// 获取文件路径
const getFilePath = (ext) => {
let filePath = null;
for (const name of ptName) {
const tempPath = path.join(tempDir, `${ name }.${ ext }`);
if (fs.existsSync(tempPath)) {
filePath = tempPath;
break;
}
}
return filePath;
};
// 在解压后的目录中查找文件
const jsonPath = getFilePath("json");
const cssPath = getFilePath("css");
const ttfPath = getFilePath("ttf");
if (!ttfPath) {
console.warn(`⚠️跳过 ${ folderName }: 缺少 TTF 文件`);
return null;
}
let iconData = [];
let iconPrefix = "";
// 优先使用JSON文件
if (jsonPath) {
iconData = processIconData(jsonPath);
if (originalFolderName !== "iconfont") {
iconPrefix = getIconPrefix(jsonPath);
}
}
// 如果没有则尝试CSS文件
else if (cssPath) {
iconData = extractIconsFromCSS(cssPath);
} else {
console.warn(`⚠️ 跳过 ${ folderName }: 缺少 ${ jsonPath }${ cssPath }`);
return null;
}
if (iconData.length === 0) {
console.warn(`⚠️ ${ folderName }: 没有找到图标数据`);
return null;
}
console.log(`${ zipFileName } 找到 ${ iconData.length } 个图标`);
// 转换TTF为base64
const base64Data = ttfToBase64(ttfPath);
if (!base64Data) {
console.error(`${ folderName }: TTF转换失败`);
return null;
}
// 为该文件夹创建icons子目录 (使用原始文件夹名称)
ensureDistDir(originalFolderName);
// 生成TypeScript文件 (使用驼峰命名作为变量名,原始名称作为路径)
generateTypeScript(originalFolderName, folderName, iconData, iconPrefix);
// 生成SCSS文件 (使用原始名称作为路径和字体名称)
generateSCSS(originalFolderName, base64Data);
return { originalName: originalFolderName, camelName: folderName };
}
// 生成主index.ts文件
function generateIndexTS(actualFolders) {
const imports = actualFolders
.map((folder) => {
const camelName = toCamelCase(folder);
return `import { ${ camelName } } from "./${ folder }";`;
})
.join("\n");
const exports = `export const icons = {\n${ actualFolders
.map((folder) => `\t${ toCamelCase(folder) }`)
.join(",\n") }\n};\n`;
const content = `${ imports }\n\n${ exports }`;
fs.writeFileSync("icons/index.ts", content);
}
// 生成主index.scss文件
function generateIndexSCSS(actualFolders) {
const imports = actualFolders.map((folder) => `@import "./${ folder }/index.scss";`).join("\n");
fs.writeFileSync("icons/index.scss", imports + "\n");
}
// 扫描icons目录下的实际文件夹
function getActualIconFolders() {
const iconsDir = "icons";
if (!fs.existsSync(iconsDir)) {
return [];
}
return fs.readdirSync(iconsDir).filter((item) => {
const itemPath = path.join(iconsDir, item);
const stats = fs.statSync(itemPath);
return stats.isDirectory();
});
}
// 主函数
function main() {
console.log("🚀 开始处理字体文件...\n");
// 确保临时目录存在
ensureTempDir();
// 确保icons目录存在
ensureDistDir();
try {
// 获取所有zip文件
const zipFiles = getZipFiles();
// 处理每个zip文件
const processedFolders = [];
for (const zipFile of zipFiles) {
const result = processZipFile(zipFile);
if (result) {
processedFolders.push(result);
}
}
// 扫描icons目录下的实际文件夹
const actualFolders = getActualIconFolders();
if (actualFolders.length > 0) {
// 生成主index文件
generateIndexTS(actualFolders);
generateIndexSCSS(actualFolders);
}
if (processedFolders.length > 0) {
const folderNames = processedFolders.map((f) =>
typeof f === "string" ? f : f.originalName
);
console.log(
`\n🎉 成功处理了 ${ processedFolders.length } 个字体包: ${ folderNames.join(", ") }`
);
}
} catch (error) {
console.error("❌ 脚本执行出错:", error);
} finally {
cleanupTempDir();
}
}
// 运行脚本
if (require.main === module) {
main();
}

View File

@@ -0,0 +1,10 @@
{
"mcpServers": {
"uni-app-x": {
"command": "npx",
"args": [
"uni-app-x-mcp"
]
}
}
}

View File

@@ -0,0 +1,13 @@
---
description: Uni-App X api
globs: *.uts,*.uvue
alwaysApply: false
---
# API
- 可以使用uts的api但注意版本和平台的兼容性。
- 可以使用uni-app x的api但注意版本和平台的兼容性。
- 可以使用vue3的api但注意版本和平台的兼容性。
- 可以使用操作系统的api但注意版本和平台的兼容性。尽量在uts插件中调用系统原生API而不是在uvue页面中直接调用系统原生API。
- 特定平台或特定版本以上才能使用的代码,需使用条件编译包围这些代码,或者放置在平台专用的目录文件中。
- 通过mcp工具查询项目下可用的插件。
- 跨页面通信优先使用eventbus。

View File

@@ -0,0 +1,37 @@
---
description: Uni-App X conditional compilation
globs: *.uts,*.uvue,*.css,*.less,*.scss,*.ts,*.js,*.sass,*.json
alwaysApply: false
---
# conditional compilation
## core syntax
```
// Platform basic judgment
#ifdef APP || MP
//Mini programs/APP common code
#ifdef APP-ANDROID
// Android-specific logic
#endif
#ifdef APP-IOS
// IOS-specific logic
#endif
#endif
```
## Core Platform Identifier
uniVersion is used to distinguish the version of the compiler Details HBuilderX 3.9.0
APP App
APP-ANDROID App Android Platform Details
APP-IOS App iOS Platform Details
APP-HARMONY App HarmonyOS Next platform
WEB web (same as H5) HBuilderX 3.6.3
MP-WEIXIN WeChat Mini Program
MP-ALIPAY APPLET
MP-BAIDU BAIDU MINI PROGRAM
MP-TUTIAO TIKTOK MINI PROGRAM
MP-KUAISHOU Kuaishou Mini Program
MP-JD JD Mini Program
MP-HARMONY Harmony Atom Service HBuilderX 4.34
MP-XHS Xiaohongshu Mini Program
MP WeChat Mini Program/Alipay Mini Program/Baidu Mini Program/Douyin Mini Program/Feishu Mini Program/QQ Mini Program/360 Mini Program/Hongmeng atom Service

View File

@@ -0,0 +1,46 @@
---
description:
globs: *.uts,*.uvue
alwaysApply: false
---
# ts | uts
- 所有方法必须在被调用之前进行定义,避免出现“先调用后定义”的情况。
- 当使用 {} 进行对象赋值时,必须显式声明类型,例如:{} as Data确保类型安全与规范。
# setup
- 页面参数请通过 props 接收,避免使用 onLoad 接口。
# <template>
- 页面无需手动在最外层包裹 <scroll-view class="flex-1" scroll-y>,因已由 <cl-page> 统一处理页面滚动。
# cool-ui
- 组件参数值必须为合法、准确且符合预期的类型和取值范围,避免传递无效或异常数值。
# 图标
- 图标样式名称的使用请参考 /icons 目录下已定义的命名。
# 设计规范
- 组件开发请参考 /pages/demo 下的代码示例,遵循其用法与风格。
- 页面开发建议参照 /pages/template 下的标准模板,保持结构与规范一致。
- 当页面底部需要展示按钮等操作区域时,建议统一使用 cl-footer 组件配合实现,确保交互区域风格和布局规范一致。
# 页面模板代码
```uvue
<template>
<cl-page>
<view class="p-3"></view>
</cl-page>
</template>
<script lang="ts" setup>
</script>
```

View File

@@ -0,0 +1,42 @@
---
description: Uni-App X implements a subset of Web CSS on the App platform
globs: *.css,*.ucss,*.uvue,*.scss,*.less,*. sass
alwaysApply: false
---
# css rules
ucss是css的子集但可以跨平台使用。除了浏览器之外还支持App原生平台。
## 布局规范
- 禁用浮动、网格等布局仅使用flex布局或绝对定位。
- flex布局默认方向为垂直(通过 flex-direction:column 实现)。
## 选择器规则
- 仅支持基本的类选择器 (.class),禁止使用其他选择器。
- 类名必须符合 [A-Za-z0-9_-]+ 规范,禁止使用特殊字符(例如 @class)。
## 文字样式规则
- 文字内容需放置在组件 <text> 或 <button> 中。文字类样式(color、font-size)只能设置在 <text> 或 <button> 组件上。 其他组件(如<view>)禁止设置文本相关样式。
- 文字样式不继承。
- 禁用继承相关关键字,例如 inherit 和 unset。
## 层级控制
- z-index 仅对同级兄弟节点生效。
- absolute 固定位与文档流分离,不支持分层覆盖。
## 长度单位
- 仅支持px、rpx、百分比。字体的line-height支持em。不能使用其他单位如vh。
- 除非width需要根据屏幕宽度而变化才使用rpx单位。其他场景不使用rpx单位。
- 除非长度单位需要根据父容器大小而变化才使用百分比单位。其他场景不使用rpx单位。
## at-rules
- 仅支持`@font-face`、`@import`不使用其他at-rules。
- 如需使用`@media` 适配不同屏幕,改用 uts 代码实现先通过API `uni.getWindowInfo`获取屏幕宽度, 再通过代码进行适配。
- 如需使用`@media` 适配暗黑模式, 改用 uts 代码 和 css变量 实现。
- 如需使用`@keyframes`改为通过UniElement对象的animate方法实现相同逻辑。
## css function
- 仅支持 url()、rgb()、rgba()、var()、env()不使用其他css方法。
## 样式作用范围规则
- 不使用css scoped。

View File

@@ -0,0 +1,25 @@
---
description: Best practices for uni-app-x
globs:
alwaysApply: true
---
# Memory Bank
你熟悉 uni-app x框架擅长编写跨平台且高性能的代码。
uni-app x项目使用UTS语言编写script。 UTS是一种跨平台的强类型语言类似TS语言但类型要求更加严格。
## Code Style and Structure
- 简洁易懂,复杂的代码配上中文注释。
- 严格类型匹配,不使用隐式转换。
- 不使用变量和函数的声明提升,严格的在清晰的范围内使用变量和函数。
- 当生成某个平台专用代码时,应使用条件编译进行平台约束,避免干扰其他平台。
## project
- 遵循uni-app x的项目结构在正确的目录中放置生成的文件。
## page
- 使用uvue作为页面后缀名uvue与vue基本类似但有少量细节差异。
- 生成的uvue页面放置在项目的pages目录下生成的页面需要在pages.json中注册。
- 可滚动内容必须在scroll-view、list-view、waterflow等滚动容器中。如果页面需要滚动则在页面template的一级子节点放置滚动容器例如` <scroll-view style="flex:1">`。 此时应在 App 上使用条件编译,例如:`<!-- #ifdef APP --><scroll-view class="container"><!-- #endif -->`。
- 生成uvue页面时页面内容需符合uts.mdc、uvue.mdc、ucss.mdc、api.mdc约定的规范。

View File

@@ -0,0 +1,28 @@
---
description:
globs: *.uts,*.uvue
alwaysApply: false
---
# UTS Rules
- 生成的脚本代码使用跨平台的UTS语言。
- UTS语言类似ts但为了跨平台编译为kotlin、swift等强类型语言进行了约束。
- UTS是强类型语言类型要求严格不能动态转换类型。 与kotlin等强类型语言一样。
- 不能使用类型隐式转换。 尤其是条件语句(if、while、do-while、三元运算符、for 循环的条件部分)必须使用布尔类型作为条件。 当判断变量a是否为空时不能写成 `if (a)`,或`if (!a)` 要写成 `if (a!=null)`。
- 可为null和不可为null的类型需要严格区分使用 `|null` 或 `?` 来定义可为空。
- 可为null的数据类型在使用其属性或方法时需要判断不为null或者使用`?.`安全调用。 谨慎使用 `!.` 断言。
- any类型的变量在使用其属性或方法时需要as为正确的相容类型。
- 不支持object类型使用UTSJSONObject类型替代。
- 不支持undefined变量使用前必须赋值。
- 对象类型定义使用type而不是interface。 interface是接口不用于对象类型定义。
- 变量和常量定义使用let和const不使用var。
- 不使用 JSX 表达式。
- 不使用 with 语句。
- 不使用ts的结构化类型系统。使用名义类型系统强调类型名称和继承关系以确保类型安全。
- 不使用 is 运算符。 使用 instanceof 和 as 进行类型保护。
- 尽量不使用any。
- 尽量不使用 === 和!== 使用 == 和!= 替代。
- 不使用js的原型链特性。
- 严格遵守“先定义后使用”的规则。使用代码在定义代码之前。
- 更多参考:[uts与ts的差异](https://doc.dcloud.net.cn/uni-app-x/uts/uts_diff_ts.html)

View File

@@ -0,0 +1,23 @@
---
description: Utz, Ounitec Scripter, Is-Akros-Platfom, Hai Performans, Stanley Tepdemodern, Programine Langguag
globs: *.uvue
alwaysApply: false
---
# uvue rules
## vue support
- 仅使用vue3语法避免使用vue2。
- 新页面尽量使用组合式API。
- 组件尽量使用easycom规范。
- 非easycom的自定义vue组件调用组件方法时需使用组件实例的`$callMethod`方式调用。
- 不使用 pinia、vuex、i18n 等uni-app x不支持的vue插件。
- 使用vue语法时需注意uni-app x官网的平台和版本兼容性平台特殊代码需包裹在条件编译中。
## component
- 组件可使用uni-app x内置组件以及项目下的自定义组件。通过mcp工具查询项目下可用的easycom插件。
- 项目可使用vuejs组件规范对应的文件扩展名为uvue。
- 符合easycom规范的组件无需import和注册可直接在template中使用。
- 使用内置组件时需注意uni-app x官网的平台和版本兼容性平台特殊代码需包裹在条件编译中。

17
cool-unix/.gitignore vendored Normal file
View File

@@ -0,0 +1,17 @@
.DS_Store
node_modules/
unpackage/
/dist/
# Log files
npm-debug.log*
# Editor directories and files
.project
.idea
.hbuilderx
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw*

30
cool-unix/.prettierrc Normal file
View File

@@ -0,0 +1,30 @@
{
"tabWidth": 4,
"useTabs": true,
"semi": true,
"jsxBracketSameLine": true,
"singleQuote": false,
"printWidth": 100,
"trailingComma": "none",
"plugins": [
{
"name": "uts",
"parsers": ["typescript"],
"vscodeLanguageIds": ["uts"]
}
],
"overrides": [
{
"files": "*.uvue",
"options": {
"parser": "vue"
}
},
{
"files": "*.uts",
"options": {
"parser": "typescript"
}
}
]
}

22
cool-unix/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,22 @@
{
"files.associations": {
"*.uvue": "vue",
"*.uts": "uts"
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[uts]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[uvue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"prettier.documentSelectors": ["**/*.uvue", "**/*.uts", "**/*.ts"],
"prettier.requireConfig": true,
"[ets]": {
"editor.defaultFormatter": "NailyZero.vscode-naily-ets"
},
"scss.lint.unknownAtRules": "ignore",
"[xml]": {
"editor.defaultFormatter": "redhat.vscode-xml"
}
}

124
cool-unix/.vscode/template.code-snippets vendored Normal file
View File

@@ -0,0 +1,124 @@
{
"page": {
"prefix": "page",
"scope": "vue",
"body": [
"<template>",
" <cl-page>",
" <view class=\"p-3\">",
" $1",
" </view>",
" </cl-page>",
"</template>",
"",
"<script lang=\"ts\" setup>",
"import { router } from \"@/cool\";",
"",
"</script>",
"",
"<style lang=\"scss\" scoped>",
"",
"</style>",
""
],
"description": "page snippets"
},
"popup": {
"prefix": "popup",
"scope": "vue",
"body": [
"<template>",
" <cl-popup v-model=\"visible\">",
" <view class=\"component$1\"> </view>",
" </cl-popup>",
"</template>",
"",
"<script lang=\"ts\" setup>",
"import { ref } from \"vue\";",
"import { router } from \"@/cool\";",
"",
"// 是否可见",
"const visible = ref(false);",
"",
"// 打开",
"function open() {",
" visible.value = true;",
"}",
"",
"// 关闭",
"function close() {",
" visible.value = false;",
"}",
"",
"defineExpose({",
" open,",
" close,",
"});",
"</script>",
"",
"<style lang=\"scss\" scoped>",
"",
"</style>",
""
],
"description": "popup snippets"
},
"pager": {
"prefix": "list-view-refresh",
"scope": "vue",
"body": [
"<template>",
" <cl-list-view",
" :data=\"listView\"",
" :virtual=\"false\"",
" :pt=\"{",
" refresher: {",
" className: 'pt-3'",
" }",
" }\"",
" :refresher-enabled=\"true\"",
" @pull=\"onPull\"",
" @bottom=\"loadMore\"",
" >",
" <template #item=\"{ value }\">",
" <view class=\"p-3\">",
" $1",
" </view>",
" </template>",
" </cl-list-view>",
"</template>",
"",
"<script lang=\"ts\" setup>",
"import { ref } from \"vue\";",
"import { usePager, request } from \"@/cool\";",
"import { useUi } from \"@/uni_modules/cool-ui\";",
"",
"const ui = useUi();",
"",
"const listViewRef = ref<ClListViewComponentPublicInstance | null>(null);",
"",
"const { refresh, listView, loading, loadMore } = usePager((params, { render }) => {",
" request({ $1 }).then((res) => {",
" if (res != null) {",
" render(res);",
" }",
" });",
"});",
"",
"async function onPull() {",
" await refresh({ page: 1 });",
" listViewRef.value!.stopRefresh();",
"}",
"",
"onReady(() => {",
" ui.showLoading(\"加载中\");",
" refresh({",
" page: 1,",
" size: 20,",
" });",
"});",
""
],
"description": "list-view refresh snippets"
}
}

54
cool-unix/App.uvue Normal file
View File

@@ -0,0 +1,54 @@
<script lang="ts">
import { useStore } from "@/cool";
// #ifdef H5
import TouchEmulator from "hammer-touchemulator";
// 模拟移动端调试的触摸事件
TouchEmulator();
// #endif
export default {
onLaunch: function () {
console.log("App Launch");
},
onShow: function () {
console.log("App Show");
// 根据业务情况判断是否要预先调用
const { user } = useStore();
// 获取用户信息,未登录不执行
user.get();
},
onHide: function () {
console.log("App Hide");
},
onExit: function () {
console.log("App Exit");
}
};
</script>
<style lang="scss">
@import url("static/index.scss");
@import url("icons/index.scss");
.safe-area-top {
margin-top: env(safe-area-inset-top);
}
.uni-tabbar {
// #ifdef H5
display: none;
// #endif
.uni-tabbar__icon {
margin-top: 0;
}
}
.uni-toast {
border-radius: 32rpx;
background-color: rgba(0, 0, 0, 0.8) !important;
}
</style>

33
cool-unix/LICENSE Normal file
View File

@@ -0,0 +1,33 @@
MIT License
Copyright (c) [2025] [厦门闪酷科技开发有限公司]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
MIT 许可证
版权所有 (c) [2025] [厦门闪酷科技开发有限公司]
特此免费授予获得本软件及相关文档文件(“软件”)副本的任何人无限制地处理本软件的权限,包括但不限于使用、复制、修改、合并、发布、分发、再许可和/或销售软件的副本,并允许软件提供给其的人员这样做,但须符合以下条件:
上述版权声明和本许可声明应包含在软件的所有副本或主要部分中。
本软件按“原样”提供,不提供任何明示或暗示的担保,包括但不限于对适销性、特定用途适用性和非侵权的担保。在任何情况下,作者或版权持有人均不对因软件或软件使用或其他交易而产生的任何索赔、损害或其他责任负责,无论是在合同诉讼、侵权诉讼或其他诉讼中。

149
cool-unix/README.md Normal file
View File

@@ -0,0 +1,149 @@
<div align="center">
# 🚀 Cool Unix
**基于 uni-app x 的跨端应用开发脚手架**
_一次开发全端运行 - 为您的创新想法提供最强大的技术底座_
[![GitHub license](https://img.shields.io/badge/license-MIT-green?style=flat-square)](https://github.com/cool-team-official/cool-unix/blob/master/LICENSE)
[![GitHub release](https://img.shields.io/github/package-json/v/cool-team-official/cool-unix?style=flat-square&color=blue)](https://github.com/cool-team-official/cool-unix/releases)
[![GitHub stars](https://img.shields.io/github/stars/cool-team-official/cool-unix?style=flat-square&color=yellow)](https://github.com/cool-team-official/cool-unix/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/cool-team-official/cool-unix?style=flat-square&color=orange)](https://github.com/cool-team-official/cool-unix/network)
[![Last commit](https://img.shields.io/github/last-commit/cool-team-official/cool-unix?style=flat-square&color=red)](https://github.com/cool-team-official/cool-unix/commits)
</div>
### 项目概述
Cool Unix 是一个高效的项目脚手架。它内置了 UI 组件库、Service 请求、TailwindCSS 插件、多语言一键翻译等多种实用功能,极大提升了开发者的开发效率与体验。
- [📖 在线文档](https://unix.cool-js.com/)
- [🎯 快速开始](https://unix.cool-js.com/src/introduce/quick.html)
- [🌟 在线预览](https://unix.cool-js.com/demo)
### 组件库引入
如果你只需使用组件库,请参考 [🚀 组件库引入指南](https://unix.cool-js.com/src/introduce/uni-components.html) 进行配置,快速集成到你的项目中。
### 多语言
配置完成后仅需执行一条命令AI 即可自动检索并统一翻译全文内容,无需手动维护繁琐的中英对照表,大幅提升多语言开发效率。
```html
<text>{{ t('你好') }}</text>
```
在其他位置上绑定如下:
```html
<script setup lang="ts">
import { $t, t } from "@/uni_modules/cool-ui";
import { useUi } from "@/uni_modules/cool-ui";
const ui = useUi();
ui.showToast({
message: t("操作成功")
});
ui.showToast({
message: $t("欢迎回来,{name}", { name: "神仙都没用" })
});
</script>
```
```shell
npx cool-i18n create
```
### TailwindCSS
不同于其他 UI 组件库仅内置简单样式Cool Unix 深度兼容 TailwindCSS 的写法,支持如 `dark:``!` 等操作符,既保留了灵活性,也便于扩展。
```html
<view class="bg-surface-100 dark:!bg-surface-900">
<text class="text-surface-700 dark:!text-white">Cool Unix</text>
</view>
```
### PassThrough
PassThrough 是一种用于访问组件内部 DOM 结构的 API它允许开发者将任意属性和监听器直接应用于组件内部的 DOM 元素。这种设计的核心优势在于突破了组件主要 API 的限制,提供更灵活的定制能力。
```html
<cl-button
:pt="{
className: '!rounded-2xl',
icon: {
size: 50,
className: 'mr-5',
},
label: {
color: 'red',
className: 'font-bold',
},
loading: {
size: 50,
},
}"
>
点击
</cl-button>
```
### 预览
<table>
<tr>
<td align="center">
<img src="https://unix.cool-js.com/qrcode-h5.png" width="200px" /><br/>
H5 预览
</td>
<td align="center">
<img src="https://unix.cool-js.com/qrcode-apk.png" width="200px" /><br/>
APP 下载
</td>
</tr>
</table>
### 技术栈
<div align="center">
### 核心框架
![uni-app x](https://img.shields.io/badge/uni--app%20x-2CA5E0?style=for-the-badge&logo=vue.js&logoColor=white)
![Vue 3](https://img.shields.io/badge/Vue%203-4FC08D?style=for-the-badge&logo=vue.js&logoColor=white)
![TypeScript](https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white)
### UI & 样式
![Tailwind CSS](https://img.shields.io/badge/Tailwind%20CSS-38B2AC?style=for-the-badge&logo=tailwind-css&logoColor=white)
![Cool UI](https://img.shields.io/badge/Cool%20UI-FF6B6B?style=for-the-badge&logo=componentstore&logoColor=white)
### 开发工具
![Vite](https://img.shields.io/badge/Vite-B73BFE?style=for-the-badge&logo=vite&logoColor=FFD62E)
![ESLint](https://img.shields.io/badge/ESLint-4B32C3?style=for-the-badge&logo=eslint&logoColor=white)
![Prettier](https://img.shields.io/badge/Prettier-F7B93E?style=for-the-badge&logo=prettier&logoColor=black)
</div>
### Star History
<div align="center">
[![Star History Chart](https://api.star-history.com/svg?repos=cool-team-official/cool-unix&type=Date)](https://star-history.com/#cool-team-official/cool-unix&Date)
</div>
### 参与贡献
我们欢迎所有形式的贡献无论是新功能、Bug 修复、文档改进还是其他任何改进。
### 开源协议
本项目基于 [MIT 协议](LICENSE) 开源,您可以自由使用、修改和分发。

View File

@@ -0,0 +1,84 @@
<template>
<cl-select
ref="selectRef"
v-model="active"
:options="options"
:show-trigger="false"
:title="t('切换语言')"
:cancel-text="t('取消')"
:confirm-text="t('确定')"
></cl-select>
</template>
<script setup lang="ts">
import { locale, setLocale, t } from "@/locale";
import { useUi, type ClSelectOption } from "@/uni_modules/cool-ui";
import { ref } from "vue";
const ui = useUi();
// 语言列表
const options = [
{
label: "简体中文",
value: "zh-cn"
},
{
label: "繁体中文",
value: "zh-tw"
},
{
label: "English",
value: "en"
},
{
label: "Español",
value: "es"
},
{
label: "日本語",
value: "ja"
},
{
label: "한국어",
value: "ko"
},
{
label: "Français",
value: "fr"
}
] as ClSelectOption[];
const selectRef = ref<ClSelectComponentPublicInstance | null>(null);
// 当前语言
const active = ref(locale.value);
// 打开
function open() {
active.value = locale.value;
if (["zh-Hans", "zh"].some((e) => e == locale.value)) {
active.value = "zh-cn";
}
selectRef.value!.open((value) => {
ui.showLoading(t("切换中"));
setTimeout(() => {
setLocale(value as string);
ui.hideLoading();
}, 500);
});
}
// 关闭
function close() {
selectRef.value!.close();
}
defineExpose({
open,
close
});
</script>

View File

@@ -0,0 +1,94 @@
<template>
<cl-select
ref="selectRef"
v-model="size"
:title="t('全局字号')"
:options="list"
:show-trigger="false"
@changing="onChanging"
>
<template #prepend>
<view class="px-3 absolute top-0 left-0 z-10">
<cl-text
:style="{
fontSize: 28 * size + 'rpx'
}"
>{{ t("这是一段示例文字,用于预览不同字号的效果。") }}</cl-text
>
</view>
</template>
</cl-select>
</template>
<script setup lang="ts">
import { t } from "@/locale";
import { type ClSelectOption } from "@/uni_modules/cool-ui";
import { config } from "@/uni_modules/cool-ui/config";
import { ref } from "vue";
defineOptions({
name: "size-set"
});
const selectRef = ref<ClSelectComponentPublicInstance | null>(null);
// 语言列表
const list = [
{
label: "0.9",
value: 0.9
},
{
label: t("默认 1.0"),
value: 1
},
{
label: "1.1",
value: 1.1
},
{
label: "1.2",
value: 1.2
},
{
label: "1.3",
value: 1.3
},
{
label: "1.4",
value: 1.4
}
] as ClSelectOption[];
// 当前语言
const size = ref(1);
// 是否可见
const visible = ref(false);
// 打开
function open() {
visible.value = true;
size.value = config.fontSize ?? 1;
selectRef.value!.open((value) => {
config.fontSize = value == 1 ? null : (value as number);
});
}
// 关闭
function close() {
visible.value = false;
}
// 切换
function onChanging(value: number) {
size.value = value;
}
defineExpose({
visible,
open,
close
});
</script>

View File

@@ -0,0 +1,234 @@
<template>
<slot :disabled="isDisabled" :countdown="countdown" :btnText="btnText">
<cl-button text :disabled="isDisabled" @tap="open">
{{ btnText }}
</cl-button>
</slot>
<cl-popup
v-model="captcha.visible"
ref="popupRef"
direction="center"
:title="t('获取短信验证码')"
:size="500"
>
<view class="p-3 pt-2 pb-4 w-full" v-if="captcha.visible">
<view class="flex flex-row items-center">
<cl-input
v-model="code"
:placeholder="t('验证码')"
:maxlength="4"
autofocus
:clearable="false"
:pt="{
className: 'flex-1 mr-2 !h-[70rpx]'
}"
@confirm="send"
></cl-input>
<view
class="dark:!bg-surface-800 bg-surface-100 rounded-lg h-[70rpx] w-[200rpx] flex flex-row justify-center items-center"
@tap="getCaptcha"
>
<cl-loading v-if="captcha.loading" :size="28"></cl-loading>
<cl-svg
v-else
class="h-full w-full pointer-events-none"
color="none"
:src="captcha.img"
></cl-svg>
</view>
</view>
<cl-button
type="primary"
:disabled="code == ''"
:loading="captcha.sending"
:pt="{
className: '!h-[70rpx] mt-3'
}"
@tap="send"
>
{{ t("发送短信") }}
</cl-button>
</view>
</cl-popup>
</template>
<script lang="ts" setup>
import { computed, reactive, ref } from "vue";
import { useUi } from "@/uni_modules/cool-ui";
import { $t, t } from "@/locale";
import { isDark, parse, request, type Response } from "@/cool";
const props = defineProps({
phone: String
});
const emit = defineEmits(["success"]);
const popupRef = ref<ClPopupComponentPublicInstance | null>(null);
const ui = useUi();
type Captcha = {
visible: boolean;
loading: boolean;
sending: boolean;
img: string;
};
// 验证码
const captcha = reactive<Captcha>({
visible: false,
loading: false,
sending: false,
img: ""
});
// 倒计时
const countdown = ref(0);
// 是否禁用
const isDisabled = computed(() => countdown.value > 0 || props.phone == "");
// 按钮文案
const btnText = computed(() =>
countdown.value > 0 ? $t("{n}s后重新获取", { n: countdown.value }) : t("获取验证码")
);
const code = ref("");
const captchaId = ref("");
// 清空
function clear() {
code.value = "";
captchaId.value = "";
}
// 关闭
function close() {
captcha.visible = false;
captcha.img = "";
clear();
}
// 开始倒计时
function startCountdown() {
countdown.value = 60;
let timer: number = 0;
function fn() {
countdown.value--;
if (countdown.value < 1) {
clearInterval(timer);
}
}
// @ts-ignore
timer = setInterval(() => {
fn();
}, 1000);
fn();
}
// 获取图片验证码
async function getCaptcha() {
clear();
captcha.loading = true;
type Res = {
captchaId: string;
data: string;
};
await request({
url: "/app/user/login/captcha",
data: {
color: isDark.value ? "#ffffff" : "#2c3142",
phone: props.phone,
width: 200,
height: 70
}
})
.then((res) => {
if (res != null) {
const data = parse<Res>(res)!;
captchaId.value = data.captchaId;
captcha.img = data.data;
}
})
.catch((err) => {
ui.showToast({
message: (err as Response).message!
});
});
setTimeout(() => {
captcha.loading = false;
}, 200);
}
// 发送短信
async function send() {
if (code.value != "") {
captcha.sending = true;
await request({
url: "/app/user/login/smsCode",
method: "POST",
data: {
phone: props.phone,
code: code.value,
captchaId: captchaId.value
}
})
.then(() => {
ui.showToast({
message: t("短信已发送,请查收")
});
startCountdown();
close();
emit("success");
})
.catch((err) => {
ui.showToast({
message: (err as Response).message!
});
getCaptcha();
});
captcha.sending = false;
} else {
ui.showToast({
message: t("请填写验证码")
});
}
}
// 打开
function open() {
if (props.phone != "") {
if (/^(?:(?:\+|00)86)?1[3-9]\d{9}$/.test(props.phone!)) {
captcha.visible = true;
getCaptcha();
} else {
ui.showToast({
message: t("请填写正确的手机号格式")
});
}
}
}
defineExpose({
open,
send,
getCaptcha,
startCountdown
});
</script>

View File

@@ -0,0 +1,83 @@
<template>
<cl-footer
:pt="{
content: {
className: '!p-0 h-[60px]'
}
}"
>
<view class="custom-tabbar" :class="{ 'is-dark': isDark }">
<view
class="custom-tabbar-item"
v-for="item in list"
:key="item.pagePath"
@tap="router.to(item.pagePath)"
>
<cl-image
:src="path == item.pagePath ? item.icon2 : item.icon"
:height="56"
:width="56"
></cl-image>
<cl-text
v-if="item.text != null"
:pt="{
className: parseClass([
'text-xs mt-1',
[path == item.pagePath, 'text-primary-500', 'text-surface-400']
])
}"
>{{ t(item.text!) }}</cl-text
>
</view>
</view>
</cl-footer>
</template>
<script setup lang="ts">
import { ctx, isDark, parseClass, router } from "@/cool";
import { t } from "@/locale";
import { computed } from "vue";
defineOptions({
name: "custom-tabbar"
});
type Item = {
icon: string;
icon2: string;
pagePath: string;
text: string | null;
};
const path = computed(() => router.path());
// tabbar 列表
const list = computed<Item[]>(() => {
return (ctx.tabBar.list ?? []).map((e) => {
return {
icon: e.iconPath!,
icon2: e.selectedIconPath!,
pagePath: e.pagePath,
text: t(e.text?.replaceAll("%", "")!)
} as Item;
});
});
// 隐藏原生 tabBar
// #ifndef MP
if (ctx.tabBar.list != null) {
uni.hideTabBar();
}
// #endif
</script>
<style lang="scss" scoped>
.custom-tabbar {
@apply flex flex-row items-center flex-1;
&-item {
@apply flex flex-col items-center justify-center flex-1;
}
}
</style>

21
cool-unix/config/dev.ts Normal file
View File

@@ -0,0 +1,21 @@
import { get } from "@/cool";
import { proxy, value } from "./proxy";
export const dev = () => {
const host = get(proxy, `${value}.target`) as string;
let baseUrl: string;
// #ifdef H5
baseUrl = `/${value}`;
// #endif
// #ifndef H5
baseUrl = host + "";
// #endif
return {
host,
baseUrl
};
};

46
cool-unix/config/index.ts Normal file
View File

@@ -0,0 +1,46 @@
import { isMp } from "@/cool";
import { dev } from "./dev";
import { prod } from "./prod";
// 判断当前是否为开发环境
export const isDev = process.env.NODE_ENV == "development";
// 忽略 token 校验的接口路径
export const ignoreTokens: string[] = [];
// 微信配置
type WxConfig = {
debug: boolean;
};
// 配置类型定义
type Config = {
name: string; // 应用名称
version: string; // 应用版本
locale: string; // 应用语言
website: string; // 官网地址
host: string; // 主机地址
baseUrl: string; // 基础路径
showDarkButton: boolean; // 是否显示暗色模式切换按钮
isCustomTabBar: boolean; // 是否自定义 tabBar
backTop: boolean; // 是否显示回到顶部按钮
wx: WxConfig; // 微信配置
};
// 根据环境导出最终配置
export const config = {
name: "Cool Unix",
version: "1.0.0",
locale: "zh",
website: "https://cool-js.com",
showDarkButton: isMp() ? false : true,
isCustomTabBar: true,
backTop: true,
wx: {
debug: false
},
...(isDev ? dev() : prod())
} as Config;
// 导出代理相关配置
export * from "./proxy";

21
cool-unix/config/prod.ts Normal file
View File

@@ -0,0 +1,21 @@
import { get } from "@/cool";
import { proxy } from "./proxy";
export const prod = () => {
const host = get(proxy, `prod.target`) as string;
let baseUrl: string;
// #ifdef H5
baseUrl = host + "/api";
// #endif
// #ifndef H5
baseUrl = host + "/api";
// #endif
return {
host,
baseUrl
};
};

21
cool-unix/config/proxy.ts Normal file
View File

@@ -0,0 +1,21 @@
export const proxy = {
// 开发环境配置
dev: {
// 官方测试地址
// target: "https://show.cool-admin.com/api",
// 本地地址
target: "http://127.0.0.1:8001",
changeOrigin: true,
rewrite: (path: string) => path.replace("/dev", "")
},
// 生产环境配置
prod: {
// 官方测试地址
target: "https://show.cool-admin.com",
changeOrigin: true,
rewrite: (path: string) => path.replace("/prod", "/api")
}
};
export const value = "dev";

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,93 @@
import { isArray, parse } from "../utils";
type Page = {
path: string;
style?: UTSJSONObject;
meta?: UTSJSONObject;
};
type SubPackage = {
root: string;
pages: Page[];
};
export type TabBarItem = {
text?: string;
pagePath: string;
iconPath?: string;
selectedIconPath?: string;
visible?: boolean;
};
export type TabBar = {
custom?: boolean;
color?: string;
selectedColor?: string;
backgroundColor?: string;
borderStyle?: string;
blurEffect?: "dark" | "extralight" | "light" | "none";
list?: TabBarItem[];
position?: "top" | "bottom";
fontSize?: string;
iconWidth?: string;
spacing?: string;
height?: string;
backgroundImage?: string;
backgroundRepeat?: "repeat" | "repeat-x" | "repeat-y" | "no-repeat";
redDotColor?: string;
};
export type Ctx = {
appid: string;
globalStyle: UTSJSONObject;
pages: Page[];
uniIdRouter: UTSJSONObject;
theme: UTSJSONObject;
tabBar: TabBar;
subPackages: SubPackage[];
SAFE_CHAR_MAP_LOCALE: string[][];
color: UTSJSONObject;
};
// 初始化 ctx 对象,不可修改!!
export const ctx = parse<Ctx>({})!;
console.log(ctx);
// PAGES 用于存储所有页面的路径及样式信息
export let PAGES: Page[] = [...ctx.pages];
// 遍历 ctx.subPackages将所有子包下的页面信息合并到 PAGES 中
if (isArray(ctx.subPackages)) {
ctx.subPackages.forEach((a) => {
a.pages.forEach((b) => {
PAGES.push({
path: a.root + "/" + b.path, // 拼接子包根路径和页面路径
style: b.style,
meta: b.meta
});
});
});
}
// 确保每个页面路径都以 "/" 开头,符合 uni-app x 规范
PAGES.forEach((e) => {
if (!e.path.startsWith("/")) {
e.path = "/" + e.path;
}
});
// TABS 用于存储 tabBar 配置项
export let TABS: TabBarItem[] = [];
// 如果 tabBar 配置存在且列表不为空,则初始化 TABS
if (ctx.tabBar.list != null) {
TABS = ctx.tabBar.list;
// 确保每个 tabBar 页面的路径都以 "/" 开头
TABS.forEach((e) => {
if (!e.pagePath.startsWith("/")) {
e.pagePath = "/" + e.pagePath;
}
});
}

View File

@@ -0,0 +1,28 @@
import { reactive, watch } from "vue";
import { isDark } from "../theme";
type CacheData = {
key: number;
};
type UseCache = {
cache: CacheData;
};
export const useCache = (source: () => any[]): UseCache => {
const cache = reactive<CacheData>({
key: 0
});
watch(source, () => {
cache.key++;
});
watch(isDark, () => {
cache.key++;
});
return {
cache
};
};

View File

@@ -0,0 +1,6 @@
export * from "./cache";
export * from "./long-press";
export * from "./pager";
export * from "./parent";
export * from "./refs";
export * from "./wx";

View File

@@ -0,0 +1,100 @@
import { vibrate } from "@/uni_modules/cool-vibrate";
import { onUnmounted, ref, type Ref } from "vue";
// 长按触发延迟时间,单位毫秒
const DELAY = 500;
// 长按重复执行间隔时间,单位毫秒
const REPEAT = 100;
/**
* 长按操作钩子函数返回类型
*/
type UseLongPress = {
// 开始长按
start: (cb: () => void) => void;
// 停止长按
stop: () => void;
// 清除定时器
clear: () => void;
// 是否正在长按中
isPressing: Ref<boolean>;
};
/**
* 长按操作钩子函数
* 支持长按持续触发,可用于数字输入框等需要连续操作的场景
*/
export const useLongPress = (): UseLongPress => {
// 是否正在长按中
const isPressing = ref(false);
// 长按延迟定时器
let pressTimer: number = 0;
// 重复执行定时器
let repeatTimer: number = 0;
/**
* 清除所有定时器
* 重置长按状态
*/
const clear = () => {
// 清除长按延迟定时器
if (pressTimer != 0) {
clearTimeout(pressTimer);
pressTimer = 0;
}
// 清除重复执行定时器
if (repeatTimer != 0) {
clearInterval(repeatTimer);
repeatTimer = 0;
}
// 重置长按状态
isPressing.value = false;
};
/**
* 开始长按操作
* @param cb 长按时重复执行的回调函数
*/
const start = (cb: () => void) => {
// 清除已有定时器
clear();
// 立即执行一次回调
cb();
// 延迟500ms后开始长按
// @ts-ignore
pressTimer = setTimeout(() => {
// 震动
vibrate(1);
// 设置长按状态
isPressing.value = true;
// 每100ms重复执行回调
// @ts-ignore
repeatTimer = setInterval(() => {
cb();
}, REPEAT);
}, DELAY);
};
/**
* 停止长按操作
* 清除定时器并重置状态
*/
const stop = () => {
clear();
};
// 组件卸载时清理定时器
onUnmounted(() => {
clear();
});
return {
start,
stop,
clear,
isPressing
};
};

View File

@@ -0,0 +1,113 @@
import { computed, ref } from "vue";
import { assign, parse } from "../utils";
import { useListView, type ClListViewItem } from "@/uni_modules/cool-ui";
// 分页参数类型
type Pagination = {
page: number; // 当前页码
size: number; // 每页数量
total: number; // 总数量
};
// 分页响应数据类型
type PagerResponse = {
list: UTSJSONObject[]; // 列表数据
pagination: Pagination; // 分页信息
};
// 分页回调函数类型
type PagerCallback = (params: UTSJSONObject, ctx: Pager) => void | Promise<void>;
// 分页器类
export class Pager {
public page = 1; // 当前页码
public size = 20; // 每页数量
public total = 0; // 总数量
public list = ref<UTSJSONObject[]>([]); // 列表数据
public loading = ref(false); // 加载状态
public refreshing = ref(false); // 刷新状态
public finished = ref(false); // 是否加载完成
public params = {} as UTSJSONObject; // 请求参数
public cb: PagerCallback | null = null; // 回调函数
// 构造函数
constructor(cb: PagerCallback) {
this.cb = cb;
}
// 完成加载
done() {
this.loading.value = false;
}
// 清空数据
clear() {
this.list.value = [];
this.finished.value = false;
this.refreshing.value = false;
this.loading.value = false;
}
// 渲染数据
public render = (res: any) => {
const { list, pagination } = parse<PagerResponse>(res)!;
// 更新分页信息
this.page = pagination.page;
this.size = pagination.size;
this.total = pagination.total;
// 更新列表数据
if (this.params.page == 1) {
this.list.value = [...list];
} else {
this.list.value.push(...list);
}
// 更新加载完成状态
this.finished.value = this.list.value.length >= this.total;
// 完成加载
this.done();
};
// 刷新数据
public refresh = async (params: UTSJSONObject) => {
// 合并参数
this.params = assign(this.params, params);
// 构建请求参数
const data = {
page: this.page,
size: this.size,
...this.params
};
// 开始加载
this.loading.value = true;
// 发起请求
await this.cb!(data, this);
};
// 加载更多数据
public loadMore = () => {
if (this.loading.value || this.finished.value) {
return;
}
this.refresh({
page: this.page + 1
});
};
// 列表视图数据
public listView = computed<ClListViewItem[]>(() => {
return useListView(this.list.value);
});
}
// 创建分页器实例
export const usePager = (cb: PagerCallback): Pager => {
return new Pager(cb);
};

View File

@@ -0,0 +1,22 @@
import { getCurrentInstance } from "vue";
/**
* 获取父组件
* @param name 组件名称
* @example useParent<ClFormComponentPublicInstance>("cl-form")
* @returns 父组件
*/
export function useParent<T>(name: string): T | null {
const { proxy } = getCurrentInstance()!;
let p = proxy?.$parent;
while (p != null) {
if (p.$options.name == name) {
return p as T | null;
}
p = p.$parent;
}
return p as T | null;
}

View File

@@ -0,0 +1,122 @@
import { reactive } from "vue";
import { isNull } from "../utils";
// #ifdef APP
// @ts-ignore
type Instance = ComponentPublicInstance | null;
// #endif
// #ifndef APP
// @ts-ignore
type Instance = any;
// #endif
/**
* Refs 类用于管理组件引用,便于在组合式 API 中获取、操作子组件实例。
*/
class Refs {
// 存储所有 ref 的响应式对象key 为 ref 名称value 为组件实例
data = reactive({} as UTSJSONObject);
/**
* 生成 ref 绑定函数,用于在模板中设置 ref。
* @param name ref 名称
* @returns 绑定函数 (el: Instance) => void
*/
set(name: string) {
return (el: Instance) => {
this.data[name] = el;
};
}
/**
* 获取指定名称的组件实例
* @param name ref 名称
* @returns 组件实例或 null
*/
get(name: string): Instance {
const d = this.data[name] as ComponentPublicInstance;
if (isNull(d)) {
return null;
}
return d;
}
/**
* 获取组件实例暴露的属性或方法(兼容不同平台)
* @param name ref 名称
* @param key 暴露的属性名
* @returns 属性值或 null
*/
getExposed<T>(name: string, key: string): T | null {
// #ifdef APP-ANDROID
const d = this.get(name);
if (isNull(d)) {
return null;
}
// 安卓平台下,$exposed 为 Map<string, any>
const ex = d!.$exposed as Map<string, any>;
if (isNull(ex)) {
return null;
}
return ex[key] as T | null;
// #endif
// #ifndef APP-ANDROID
// 其他平台直接通过属性访问
return this.get(name)?.[key] as T;
// #endif
}
/**
* 调用组件实例暴露的方法,并返回结果
* @param name ref 名称
* @param method 方法名
* @param data 传递的数据
* @returns 方法返回值
*/
call<T>(name: string, method: string, data: UTSJSONObject | null = null): T {
return this.get(name)!.$callMethod(method, data) as T;
}
/**
* 调用组件实例暴露的方法,无返回值
* @param name ref 名称
* @param method 方法名
* @param data 传递的数据
*/
callMethod(name: string, method: string, data: UTSJSONObject | null = null): void {
this.get(name)!.$callMethod(method, data);
}
/**
* 调用组件的 open 方法,常用于弹窗、抽屉等组件
* @param name ref 名称
* @param data 传递的数据
*/
open(name: string, data: UTSJSONObject | null = null) {
this.callMethod(name, "open", data);
}
/**
* 调用组件的 close 方法,常用于弹窗、抽屉等组件
* @param name ref 名称
*/
close(name: string) {
return this.callMethod(name, "close");
}
}
/**
* useRefs 组合式函数,返回 Refs 实例
* @returns Refs 实例
*/
export function useRefs(): Refs {
return new Refs();
}

247
cool-unix/cool/hooks/wx.ts Normal file
View File

@@ -0,0 +1,247 @@
import { ref } from "vue";
import { assign, getUrlParam, storage } from "../utils";
import { request } from "../service";
import { t } from "@/locale";
import { config } from "@/config";
// #ifdef H5
import wx from "weixin-js-sdk";
// #endif
// 微信配置类型
type WxConfig = {
appId: string;
};
// 微信相关功能封装类
export class Wx {
// 微信登录code
code = ref("");
/**
* 获取微信登录code
*/
async getCode(): Promise<string> {
return new Promise((resolve) => {
// #ifdef MP-WEIXIN
uni.login({
provider: "weixin",
success: (res) => {
this.code.value = res.code;
resolve(res.code);
}
});
// #endif
// #ifndef MP-WEIXIN
resolve("");
// #endif
});
}
// #ifdef H5
// 公众号配置
mpConfig: WxConfig = {
appId: ""
};
/**
* 判断当前是否为微信浏览器
*/
isWxBrowser() {
const ua: string = window.navigator.userAgent.toLowerCase();
if (ua.match(/MicroMessenger/i) != null) {
return true;
} else {
return false;
}
}
/**
* 获取公众号配置信息并初始化微信JS-SDK
*/
getMpConfig() {
if (this.isWxBrowser()) {
request({
url: "/app/user/common/wxMpConfig",
method: "POST",
data: {
url: `${location.origin}${location.pathname}`
}
}).then((res) => {
if (res != null) {
wx.config({
debug: config.wx.debug,
jsApiList: res.jsApiList || ["chooseWXPay"],
appId: res.appId,
timestamp: res.timestamp,
nonceStr: res.nonceStr,
signature: res.signature,
openTagList: res.openTagList
});
// 合并配置到mpConfig
assign(this.mpConfig, res);
}
});
}
}
/**
* 跳转到微信授权页面
*/
mpAuth() {
const { appId } = this.mpConfig;
const redirect_uri = encodeURIComponent(
`${location.origin}${location.pathname}#/pages/user/login`
);
const response_type = "code";
const scope = "snsapi_userinfo";
const state = "STATE";
const url = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appId}&redirect_uri=${redirect_uri}&response_type=${response_type}&scope=${scope}&state=${state}#wechat_redirect`;
location.href = url;
}
/**
* 公众号登录获取code
*/
mpLogin() {
return new Promise((resolve) => {
const code = getUrlParam("code");
const mpCode = storage.get("mpCode");
// 去除url中的code参数避免重复
const url = window.location.href.replace(/(\?[^#]*)#/, "#");
window.history.replaceState({}, "", url);
if (code != mpCode) {
storage.set("mpCode", code, 1000 * 60 * 5);
resolve(code);
} else {
resolve(null);
}
});
}
/**
* 公众号微信支付
* @param params 支付参数
*/
mpPay(params: wx.IchooseWXPay & { timeStamp: number }): Promise<void> {
return new Promise((resolve, reject) => {
if (!this.isWxBrowser()) {
return reject({ message: t("请在微信浏览器中打开") });
}
wx.chooseWXPay({
...params,
timestamp: params.timeStamp,
success() {
resolve();
},
complete(e: { errMsg: string }) {
switch (e.errMsg) {
case "chooseWXPay:cancel":
reject({ message: t("已取消支付") });
break;
default:
reject({ message: t("支付失败") });
}
}
});
});
}
// #endif
// #ifdef MP
/**
* 小程序登录获取用户信息和code
*/
miniLogin(): Promise<{
code: string;
iv: string;
encryptedData: string;
signature: string;
rawData: string;
}> {
return new Promise((resolve, reject) => {
// 兼容 MacMac 端需用 getUserInfo
const k = uni.getDeviceInfo().platform === "mac" ? "getUserInfo" : "getUserProfile";
uni[k]({
lang: "zh_CN",
desc: t("授权信息仅用于用户登录"),
success: ({ iv, encryptedData, signature, rawData }) => {
const next = () => {
resolve({
iv,
encryptedData,
signature,
rawData,
code: this.code.value
});
};
// 检查登录状态是否过期
uni.checkSession({
success: () => {
next();
},
fail: () => {
this.getCode().then(() => {
next();
});
}
});
},
fail: (err) => {
console.error(`[useWx.miniLogin] error`, err);
this.getCode();
reject(t("登录授权失败"));
}
});
});
}
/**
* 小程序微信支付
* @param params 支付参数
*/
miniPay(params: any): Promise<void> {
return new Promise((resolve, reject) => {
uni.requestPayment({
provider: "wxpay",
...params,
success() {
resolve();
},
fail() {
reject(t("已取消支付"));
}
});
});
}
// #endif
}
/**
* useWx 钩子函数,后续可扩展
*/
export const useWx = (): Wx => {
const wx = new Wx();
onReady(() => {
wx.getCode();
// #ifdef H5
wx.getMpConfig();
// #endif
});
return wx;
};

46
cool-unix/cool/index.ts Normal file
View File

@@ -0,0 +1,46 @@
import { watch } from "vue";
import { scroller } from "./scroller";
import { initTheme, setH5 } from "./theme";
import { initLocale, locale, updateTitle } from "@/locale";
import "@/uni_modules/cool-ui";
export function cool(app: VueApp) {
app.mixin({
onPageScroll(e) {
scroller.emit(e.scrollTop);
},
onShow() {
// 更新标题
updateTitle();
// #ifdef H5
setTimeout(() => {
setH5();
}, 0);
// #endif
},
onLoad() {
// 监听语言切换,更新标题
watch(locale, () => {
updateTitle();
});
}
});
initTheme();
initLocale();
console.log(app);
}
export * from "./animation";
export * from "./ctx";
export * from "./hooks";
export * from "./router";
export * from "./scroller";
export * from "./service";
export * from "./store";
export * from "./theme";
export * from "./upload";
export * from "./utils";
export * from "./types";

View File

@@ -0,0 +1,362 @@
import { PAGES, TABS } from "../ctx";
import type { BackOptions, PageInstance, PushOptions } from "../types";
import {
storage,
last,
isNull,
isEmpty,
get,
isFunction,
toArray,
map,
debounce,
nth,
assign,
parse
} from "../utils";
// 路由信息类型
type RouteInfo = {
path: string;
query: UTSJSONObject;
meta: UTSJSONObject;
isAuth?: boolean;
};
// 跳转前钩子类型
type BeforeEach = (to: RouteInfo, from: PageInstance, next: () => void) => void;
// 登录后回调类型
type AfterLogin = () => void;
// 路由事件集合
type Events = {
beforeEach?: BeforeEach;
afterLogin?: AfterLogin;
};
// 路由核心类
export class Router {
private eventsMap = {} as Events; // 事件存储
// 获取传递的 params 参数
params() {
return (storage.get("router-params") ?? {}) as UTSJSONObject;
}
// 获取传递的 query 参数
query() {
return this.route()?.query ?? {};
}
// 获取默认路径,支持 home 和 login
defaultPath(name: "home" | "login") {
const paths = {
home: PAGES[0].path, // 首页为第一个页面
login: "/pages/user/login"
};
return get(paths, name) as string;
}
// 获取当前页面栈的所有页面实例
getPages(): PageInstance[] {
return map(getCurrentPages(), (e) => {
let path = e.route!;
// 根路径自动转为首页
if (path == "/") {
path = this.defaultPath("home");
}
// 补全路径前缀
if (!path.startsWith("/")) {
path = "/" + path;
}
// 获取页面样式
const page = PAGES.find((e) => e.path == path);
const style = page?.style;
const meta = page?.meta;
// 获取页面暴露的方法
// @ts-ignore
const vm = e.vm as any;
let exposed = vm;
// #ifdef H5
exposed = get(e, "vm.$.exposed");
// #endif
// 获取页面 query 参数
// @ts-ignore
const query = e.options;
return {
path,
vm,
exposed,
style,
meta,
query,
isCustomNavbar: style?.navigationStyle == "custom"
} as PageInstance;
});
}
// 获取指定路径的页面实例
getPage(path: string) {
return this.getPages().find((e) => e.path == path);
}
// 获取当前路由页面实例
route() {
return last(this.getPages());
}
// 获取当前页面路径
path() {
return this.route()?.path ?? "";
}
// 简单跳转页面(默认 navigateTo
to(path: string) {
this.push({
path
});
}
// 路由跳转,支持多种模式和参数
push(options: PushOptions) {
let {
query = {},
params = {},
mode = "navigateTo",
path,
success,
fail,
complete,
animationType,
animationDuration,
events,
isAuth
} = options;
// 拼接 query 参数到 url
if (!isEmpty(query)) {
const arr = toArray(query, (v, k) => {
return `${k}=${v}`;
});
path += "?" + arr.join("&");
}
// params 通过 storage 临时存储
if (!isEmpty(params)) {
storage.set("router-params", params, 0);
}
// tabBar 页面强制使用 switchTab 跳转
if (this.isTabPage(path)) {
mode = "switchTab";
}
// 跳转执行函数
const next = () => {
switch (mode) {
case "navigateTo":
uni.navigateTo({
url: path,
success,
events,
fail,
complete,
animationType,
animationDuration
});
break;
case "redirectTo":
uni.redirectTo({
url: path,
success,
fail,
complete
});
break;
case "reLaunch":
uni.reLaunch({
url: path,
success,
fail,
complete
});
break;
case "switchTab":
uni.switchTab({
url: path,
success,
fail,
complete
});
break;
}
};
// 跳转前钩子处理
if (this.eventsMap.beforeEach != null) {
// 当前页
const from = last(this.getPages());
// 跳转页
const to = { path, meta: this.getMeta(path), query, isAuth } as RouteInfo;
// 调用跳转前钩子
this.eventsMap.beforeEach(to, from!, next);
} else {
next();
}
}
// 回到首页
home() {
this.push({
path: this.defaultPath("home")
});
}
// 返回上一页
back(options: BackOptions | null = null) {
if (this.isFirstPage()) {
this.home();
} else {
const delta = options?.delta ?? 1;
// 执行跳转函数
const next = () => {
uni.navigateBack({ ...(options ?? {}) });
};
// 跳转前钩子处理
if (this.eventsMap.beforeEach != null) {
// 当前页
const from = last(this.getPages());
// 上一页
const to = nth(this.getPages(), -delta - 1);
if (to != null) {
// 调用跳转前钩子
this.eventsMap.beforeEach(
{
path: to.path,
query: to.query,
meta: to.meta ?? ({} as UTSJSONObject)
},
from!,
next
);
} else {
console.error("[router] found to page is null");
}
} else {
next();
}
}
}
// 获取页面元数据
getMeta(path: string) {
return PAGES.find((e) => path.includes(e.path))?.meta ?? ({} as UTSJSONObject);
}
// 执行当前页面暴露的方法
callMethod(name: string, data?: any): any | null {
const fn = get(this.route()!, `$vm.$.exposed.${name}`) as (d?: any) => any | null;
if (isFunction(fn)) {
return fn(data);
}
return null;
}
// 判断页面栈是否只有一个页面
isFirstPage() {
return getCurrentPages().length == 1;
}
// 判断是否为首页
isHomePage() {
return this.path() == this.defaultPath("home");
}
// 判断是否为自定义导航栏页面
isCustomNavbarPage() {
return this.route()?.isCustomNavbar ?? false;
}
// 判断是否为当前页面
isCurrentPage(path: string) {
return this.path() == path;
}
// 判断是否为 tab 页面
isTabPage(path: string | null = null) {
if (path == null) {
path = this.path();
}
if (path == "/") {
path = this.defaultPath("home");
}
return !isNull(TABS.find((e) => path == e.pagePath));
}
// 判断是否为登录页
isLoginPage(path: string) {
return path == this.defaultPath("login");
}
// 跳转到登录页(防抖处理)
login = debounce(() => {
if (!this.isLoginPage(this.path())) {
this.push({
path: "/pages/user/login",
mode: "reLaunch"
});
}
}, 300);
// 登录成功后跳转逻辑
nextLogin() {
const pages = this.getPages();
// 找到登录页的索引
const index = pages.findIndex((e) => this.defaultPath("login").includes(e.path));
// 未找到,则跳回首页
if (index < 0) {
this.home();
} else {
this.back({
delta: pages.length - index
});
}
// 登录后回调
if (this.eventsMap.afterLogin != null) {
this.eventsMap.afterLogin!();
}
// 触发全局 afterLogin 事件
uni.$emit("afterLogin");
}
// 注册跳转前钩子
beforeEach(cb: BeforeEach) {
this.eventsMap.beforeEach = cb;
}
// 注册登录后回调
afterLogin(cb: AfterLogin) {
this.eventsMap.afterLogin = cb;
}
}
// 单例导出
export const router = new Router();

View File

@@ -0,0 +1,33 @@
import { router } from "../router";
class Scroller {
list: Map<string, ((top: number) => void)[]> = new Map();
// 触发滚动
emit(top: number) {
const cbs = this.list.get(router.path()) ?? [];
cbs.forEach((cb) => {
cb(top);
});
}
// 监听页面滚动
on(callback: (top: number) => void) {
const path = router.path();
const cbs = this.list.get(path) ?? [];
cbs.push(callback);
this.list.set(path, cbs);
}
// 取消监听页面滚动
off = (callback: (top: number) => void) => {
const path = router.path();
const cbs = this.list.get(path) ?? [];
this.list.set(
path,
cbs.filter((cb) => cb != callback)
);
};
}
export const scroller = new Scroller();

View File

@@ -0,0 +1,198 @@
import { isDev, ignoreTokens, config } from "@/config";
import { locale, t } from "@/locale";
import { isNull, isObject, parse, storage } from "../utils";
import { useStore } from "../store";
// 请求参数类型定义
export type RequestOptions = {
url: string; // 请求地址
method?: RequestMethod; // 请求方法
data?: any; // 请求体数据
params?: any; // URL参数
header?: any; // 请求头
timeout?: number; // 超时时间
withCredentials?: boolean; // 是否携带凭证
firstIpv4?: boolean; // 是否优先使用IPv4
enableChunked?: boolean; // 是否启用分块传输
};
// 响应数据类型定义
export type Response = {
code?: number;
message?: string;
data?: any;
};
// 请求队列用于等待token刷新后继续请求
let requests: ((token: string) => void)[] = [];
// 标记token是否正在刷新
let isRefreshing = false;
// 判断当前url是否忽略token校验
const isIgnoreToken = (url: string) => {
return ignoreTokens.some((e) => {
const pattern = e.replace(/\*/g, ".*");
return new RegExp(pattern).test(url);
});
};
/**
* 通用请求方法
* @param options 请求参数
* @returns Promise<T>
*/
export function request(options: RequestOptions): Promise<any | null> {
let { url, method = "GET", data = {}, params, header = {}, timeout = 60000 } = options;
const { user } = useStore();
// 开发环境下打印请求信息
if (isDev) {
console.log(`[${method}] ${url}`, params || data);
}
// 拼接基础url
if (!url.startsWith("http")) {
url = config.baseUrl + url;
}
// 处理GET请求的params参数拼接到URL上
if (method === "GET" && params) {
const queryString = Object.keys(params)
.filter(key => params[key] !== undefined && params[key] !== null)
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
.join('&');
if (queryString) {
url += (url.includes('?') ? '&' : '?') + queryString;
}
if (isDev) {
console.log(`[GET] 完整URL: ${url}`);
}
}
// 获取当前token
let Authorization: string | null = user.token;
// 如果是忽略token的接口则不携带token
if (isIgnoreToken(url)) {
Authorization = null;
}
return new Promise((resolve, reject) => {
// 发起请求的实际函数
const next = () => {
uni.request({
url,
method,
data,
header: {
Authorization,
language: locale.value,
...(header as UTSJSONObject)
},
timeout,
success(res) {
// 401 无权限
if (res.statusCode == 401) {
user.logout();
reject({ message: t("无权限") } as Response);
}
// 502 服务异常
else if (res.statusCode == 502) {
reject({
message: t("服务异常")
} as Response);
}
// 404 未找到
else if (res.statusCode == 404) {
return reject({
message: `[404] ${url}`
} as Response);
}
// 200 正常响应
else if (res.statusCode == 200) {
if (res.data == null) {
resolve(null);
} else if (!isObject(res.data as any)) {
resolve(res.data);
} else {
// 解析响应数据
const { code, message, data } = parse<Response>(
res.data ?? { code: 0 }
)!;
switch (code) {
case 1000:
resolve(data);
break;
default:
reject({ message, code } as Response);
break;
}
}
} else {
reject({ message: t("服务异常") } as Response);
}
},
// 网络请求失败
fail(err) {
reject({ message: err.errMsg } as Response);
}
});
};
// 非刷新token接口才进行token有效性校验
if (!options.url.includes("/refreshToken")) {
if (!isNull(Authorization)) {
// 判断token是否过期
if (storage.isExpired("token")) {
// 判断refreshToken是否过期
if (storage.isExpired("refreshToken")) {
// 刷新token也过期直接退出登录
user.logout();
return;
}
// 如果当前没有在刷新token则发起刷新
if (!isRefreshing) {
isRefreshing = true;
user.refreshToken()
.then((token) => {
// 刷新成功后,执行队列中的请求
requests.forEach((cb) => cb(token));
requests = [];
isRefreshing = false;
})
.catch((err) => {
reject(err);
user.logout();
});
}
// 将当前请求加入队列等待token刷新后再执行
new Promise((resolve) => {
requests.push((token: string) => {
// 重新设置token
Authorization = token;
next();
resolve(true);
});
});
// 此处return等待token刷新
return;
}
}
}
// token有效直接发起请求
next();
});
}

View File

@@ -0,0 +1,136 @@
import { reactive } from "vue";
import { request } from "../service";
import { forInObject, isNull, parse } from "../utils";
// 字典项类型定义
export type DictItem = {
id: number; // 字典项ID
typeId: number; // 字典类型ID
label: string; // 显示标签
name: string; // 可选名称
value: any; // 字典项值
orderNum: number; // 排序号
parentId?: number | null; // 父级ID可选
};
// 字典数据类型定义
export type DictData = {
key: string; // 字典key
list: DictItem[]; // 字典项列表
};
// 字典管理类
export class Dict {
private data: DictData[] = reactive([]); // 存储所有字典数据
constructor() {}
/**
* 获取指定key的字典数据
* @param key 字典key
* @returns 字典数据
*/
find(key: string) {
return this.data.find((e) => e.key == key);
}
/**
* 获取指定key的字典项列表
* @param key 字典key
* @returns 字典项数组
*/
get(key: string): DictItem[] {
return this.find(key)?.list ?? new Array<DictItem>();
}
/**
* 获取指定key和value的字典项
* @param key 字典key
* @param value 字典项值
* @returns 字典项或null
*/
getItem(key: string, value: any): DictItem | null {
const item = this.get(key).find((e) => e.value == value);
if (isNull(item)) {
return null;
}
return item!;
}
/**
* 获取指定key和多个value的字典项数组
* @param key 字典key
* @param values 字典项值数组
* @returns 字典项数组
*/
getItems(key: string, values: any[]): DictItem[] {
return values.map((e) => this.getItem(key, e)).filter((e) => !isNull(e)) as DictItem[];
}
/**
* 获取指定key和value的字典项的label
* @param key 字典key
* @param value 字典项值
* @returns 字典项label字符串
*/
getItemLabel(key: string, value: any): string {
const item = this.getItem(key, value);
if (isNull(item) || isNull(item?.label)) {
return "";
}
return item!.label;
}
/**
* 刷新字典数据
* @param types 可选指定需要刷新的字典key数组
*/
async refresh(types?: string[] | null): Promise<void> {
const res = await request({
url: "/app/dict/info/data",
method: "POST",
data: { types }
});
if (res == null) {
return;
}
// 遍历返回的字典数据
forInObject(res, (arr, key) => {
let list: DictItem[] = [];
(arr as UTSJSONObject[]).forEach((e) => {
e["label"] = e["name"];
const d = parse<DictItem>(e);
if (d != null) {
list.push(d);
}
});
const item = this.find(key);
// 如果不存在则新增,否则更新
if (isNull(item)) {
this.data.push({
key,
list
});
} else {
item!.list = list;
}
});
// #ifdef H5
console.log("[DICT]", this.data);
// #endif
}
}
// 单例字典对象
export const dict = new Dict();

View File

@@ -0,0 +1,17 @@
import { Dict, dict } from "./dict";
import { User, user } from "./user";
type Store = {
user: User;
dict: Dict;
};
export function useStore(): Store {
return {
user,
dict
};
}
export * from "./dict";
export * from "./user";

View File

@@ -0,0 +1,185 @@
import { computed, ref } from "vue";
import { forInObject, isNull, isObject, parse, storage } from "../utils";
import { router } from "../router";
import { request } from "../service";
import type { UserInfo } from "@/types";
export type Token = {
token: string; // 访问token
expire: number; // token过期时间
refreshToken: string; // 刷新token
refreshExpire: number; // 刷新token过期时间
};
export class User {
/**
* 用户信息,响应式对象
*/
info = ref<UserInfo | null>(null);
/**
* 当前token字符串或null
*/
token: string | null = null;
constructor() {
// 获取本地用户信息
const userInfo = storage.get("userInfo");
// 获取本地token
const token = storage.get("token") as string | null;
// 如果token为空字符串则置为null
this.token = token == "" ? null : token;
// 初始化用户信息
if (userInfo != null && isObject(userInfo)) {
this.set(userInfo);
}
}
/**
* 获取用户信息(从服务端拉取最新信息并更新本地)
* @returns Promise<void>
*/
async get() {
if (this.token != null) {
await request({
url: "/app/user/info/person"
})
.then((res) => {
if (res != null) {
this.set(res);
}
})
.catch(() => {
// this.logout();
});
}
}
/**
* 设置用户信息并存储到本地
* @param data 用户信息对象
*/
set(data: any) {
if (isNull(data)) {
return;
}
// 设置
this.info.value = parse<UserInfo>(data)!;
// 持久化到本地存储
storage.set("userInfo", data, 0);
}
/**
* 更新用户信息(本地与服务端同步)
* @param data 新的用户信息
*/
async update(data: any) {
if (isNull(data) || isNull(this.info.value)) {
return;
}
// 本地同步更新
forInObject(data, (value, key) => {
this.info.value![key] = value;
});
// 同步到服务端
await request({
url: "/app/user/info/updatePerson",
method: "POST",
data
});
}
/**
* 移除用户信息
*/
remove() {
this.info.value = null;
storage.remove("userInfo");
}
/**
* 判断用户信息是否为空
* @returns boolean
*/
isNull() {
return this.info.value == null;
}
/**
* 清除本地所有用户信息和token
*/
clear() {
storage.remove("userInfo");
storage.remove("token");
storage.remove("refreshToken");
this.token = null;
this.remove();
}
/**
* 退出登录,清除所有信息并跳转到登录页
*/
logout() {
this.clear();
router.login();
}
/**
* 设置token并存储到本地
* @param data Token对象
*/
setToken(data: Token) {
this.token = data.token;
// 访问token提前5秒过期防止边界问题
storage.set("token", data.token, data.expire - 5);
// 刷新token提前5秒过期
storage.set("refreshToken", data.refreshToken, data.refreshExpire - 5);
}
/**
* 刷新token调用服务端接口自动更新本地token
* @returns Promise<string> 新的token
*/
refreshToken(): Promise<string> {
return new Promise((resolve, reject) => {
request({
url: "/app/user/login/refreshToken",
method: "POST",
data: {
refreshToken: storage.get("refreshToken")
}
})
.then((res) => {
if (res != null) {
const token = parse<Token>(res);
if (token != null) {
this.setToken(token);
resolve(token.token);
}
}
})
.catch((err) => {
reject(err);
});
});
}
}
/**
* 单例用户对象,项目全局唯一
*/
export const user = new User();
/**
* 用户信息,响应式对象
*/
export const userInfo = computed(() => user.info.value);

View File

@@ -0,0 +1,253 @@
import { computed, ref } from "vue";
import uniTheme from "@/theme.json";
import { router } from "../router";
import { ctx } from "../ctx";
import { isNull } from "../utils";
// 主题类型定义,仅支持 light 和 dark
type Theme = "light" | "dark";
// 是否为自动主题模式(跟随系统)
export const isAuto = ref(true);
/**
* 获取页面样式
* @param key 样式 key
* @returns 样式值
*/
export function getStyle(key: string): string | null {
// 页面配置
const style = router.route()?.style;
// 页面配置 key 映射
const names = {
bgColor: "backgroundColor",
bgContentColor: "backgroundColorContent",
navBgColor: "navigationBarBackgroundColor",
navTextStyle: "navigationBarTextStyle"
};
// 如果页面配置存在,则使用页面配置
if (style != null) {
if (names[key] != null) {
const val = style[names[key]!] as string | null;
if (val != null) {
return val;
}
}
}
return null;
}
/**
* 获取颜色
* @param name 颜色名称
* @returns 颜色值
*/
export const getColor = (name: string) => {
if (isNull(ctx.color)) {
return "";
}
return ctx.color[name] as string;
};
/**
* 获取 uniapp 主题配置
*/
export function getConfig(key: string): string {
// 主题配置
const themeVal = ((isDark.value ? uniTheme.dark : uniTheme.light) as UTSJSONObject)[key] as
| string
| null;
// 页面样式
const styleVal = getStyle(key);
return styleVal ?? themeVal ?? "";
}
/**
* 获取当前主题
* APP 下优先获取 appTheme若为 auto 则跟随系统 osTheme
* H5/小程序下优先获取 hostTheme否则默认为 light
*/
const getTheme = () => {
let value: string | null;
// #ifdef APP
const appInfo = uni.getAppBaseInfo();
// @ts-ignore
const appTheme = appInfo.appTheme as string;
const osTheme = uni.getSystemInfoSync().osTheme!;
// 如果 appTheme 为 auto则跟随系统主题否则使用 appTheme
value = appTheme == "auto" ? osTheme : appTheme;
isAuto.value = appTheme == "auto";
// #endif
// #ifdef H5 || MP
const hostTheme = uni.getAppBaseInfo().hostTheme;
if (hostTheme) {
// 如果有 hostTheme则使用 hostTheme
value = hostTheme;
} else {
// 默认使用 light 主题
value = "light";
}
// #endif
return value as Theme;
};
// 当前主题响应式变量
export const theme = ref<Theme>(getTheme());
/**
* 是否为暗色模式
*/
export const isDark = computed(() => {
return theme.value == "dark";
});
/**
* 切换自动主题模式(仅 APP 有效)
*/
export const setIsAuto = () => {
// #ifdef APP
isAuto.value = !isAuto.value;
if (isAuto.value) {
// 设置为自动主题,跟随系统
uni.setAppTheme({
theme: "auto"
});
} else {
// 关闭自动,使用当前 theme
setTheme(theme.value);
}
// #endif
};
/**
* 设置主题
* @param value 主题值("light" | "dark"
*/
export const setTheme = (value: Theme) => {
// 如果当前主题与目标主题一致,则不做处理
if (theme.value == value) return;
// 关闭自动主题
isAuto.value = false;
// #ifdef APP
uni.setAppTheme({
theme: value,
success: () => {
// 设置成功后更新 theme
theme.value = value;
}
});
// #endif
// #ifndef APP
theme.value = value;
// #endif
// #ifdef H5
setH5();
// #endif
};
// 设置 H5 下的主题色
export const setH5 = () => {
const bgContentColor = getConfig("bgContentColor");
const tabBgColor = getConfig("tabBgColor");
const navBgColor = getConfig("navBgColor");
const navTextStyle = getConfig("navTextStyle");
document.body.style.setProperty("--background-color-content", bgContentColor);
const tabbar = document.querySelector(".uni-tabbar");
if (tabbar) {
(tabbar as HTMLElement).style.backgroundColor = tabBgColor;
}
const pageHead = document.querySelector(".uni-page-head");
if (pageHead) {
(pageHead as HTMLElement).style.backgroundColor = navBgColor;
(pageHead as HTMLElement).style.color = navTextStyle;
}
const pageHeadBtnPath = document.querySelector(".uni-page-head-btn path");
if (pageHeadBtnPath) {
(pageHeadBtnPath as HTMLElement).style.fill = navTextStyle;
}
window.parent.postMessage(
{
type: "theme-change",
isDark: isDark.value
},
"*"
);
};
/**
* 切换主题
*/
export const toggleTheme = () => {
if (isDark.value) {
setTheme("light");
} else {
setTheme("dark");
}
};
/**
* 初始化主题监听
* APP 下监听系统主题和 App 主题变化
* H5/小程序下监听 hostTheme 变化
*/
export const initTheme = () => {
// #ifdef APP-ANDROID || APP-IOS
uni.onOsThemeChange((res) => {
if (isAuto.value) {
setTimeout(() => {
uni.setAppTheme({
theme: res.osTheme,
success: () => {
theme.value = res.osTheme;
}
});
}, 100);
}
});
// 监听 App 主题变化
uni.onAppThemeChange((res) => {
theme.value = res.appTheme;
});
// #endif
// #ifdef MP
uni.onHostThemeChange((res) => {
setTheme(res.hostTheme);
});
// #endif
// #ifdef H5
// 监听父窗口发送的主题变化消息
// [BUG] uni.onHostThemeChange 打包会丢失
// uni.onHostThemeChange((res) => {
// setTheme(res.hostTheme);
// });
window.addEventListener("message", (e) => {
if (e.data?.type == "theme-change") {
setTheme(e.data.isDark ? "dark" : "light");
}
});
// #endif
};

View File

@@ -0,0 +1,58 @@
export type PushAnimationType =
| "auto"
| "none"
| "slide-in-right"
| "slide-in-left"
| "slide-in-top"
| "slide-in-bottom"
| "fade-in"
| "zoom-out"
| "zoom-fade-out"
| "pop-in";
export type BackAnimationType =
| "auto"
| "none"
| "slide-out-right"
| "slide-out-left"
| "slide-out-top"
| "slide-out-bottom"
| "fade-out"
| "zoom-in"
| "zoom-fade-in"
| "pop-out";
export type PushMode = "navigateTo" | "redirectTo" | "reLaunch" | "switchTab";
export type BackOptions = {
delta?: number;
animationType?: BackAnimationType;
animationDuration?: number;
success?: (result: any) => void;
fail?: (result: any) => void;
complete?: (result: any) => void;
};
export type PushOptions = {
path: string;
mode?: PushMode;
events?: any;
query?: UTSJSONObject;
isAuth?: boolean;
params?: UTSJSONObject;
animationType?: PushAnimationType;
animationDuration?: number;
success?: (result: any) => void;
fail?: (result: any) => void;
complete?: (result: any) => void;
};
export type PageInstance = {
path: string;
vm: any;
style?: UTSJSONObject;
query: UTSJSONObject;
exposed: any;
isCustomNavbar: boolean;
meta?: UTSJSONObject;
};

View File

@@ -0,0 +1,254 @@
import { config } from "@/config";
import { request } from "../service";
import { basename, extname, filename, parse, parseObject, pathJoin, uuid } from "../utils";
import { useStore } from "../store";
// 上传进度回调结果类型
export type OnProgressUpdateResult = {
progress: number;
totalBytesSent: number;
totalBytesExpectedToSend: number;
};
// 上传任务类型定义
export type UploadTask = {
abort(): void;
};
// 上传选项类型定义
export type UploadOptions = {
onProgressUpdate?: (result: OnProgressUpdateResult) => void; // 上传进度回调
onTask?: (task: UploadTask) => void; // 上传任务回调
};
// 上传模式类型
export type UploadMode = {
mode: "local" | "cloud"; // 上传模式:本地或云端
type: string; // 云服务类型
};
// 上传请求的参数类型
export type UploadRequestOptions = {
url: string;
preview?: string;
data: any;
};
// 云上传返回数据类型
export type CloudUploadResponse = {
uploadUrl?: string;
url?: string;
host?: string;
credentials?: any;
OSSAccessKeyId?: string;
policy?: string;
signature?: string;
publicDomain?: string;
token?: string;
fields?: any;
};
// 本地上传返回数据类型
export type LocalUploadResponse = {
code: number;
message?: string;
data: string;
};
// 获取上传模式(本地/云端及云类型)
async function getUploadMode(): Promise<UploadMode> {
const res = await request({
url: "/app/base/comm/uploadMode"
});
return parse<UploadMode>(res!)!;
}
/**
* 路径上传
* @param path 文件路径
*/
export async function upload(path: string) {
return uploadFile({
path,
size: 0,
name: "",
type: "image/png"
});
}
/**
* 文件上传
* @param file 文件信息 ChooseImageTempFile
* @param options 上传选项
*/
export async function uploadFile(
file: ChooseImageTempFile,
options: UploadOptions | null = null
): Promise<string> {
const { user } = useStore();
// 获取上传模式和类型
const { mode, type } = await getUploadMode();
// 判断是否本地上传
const isLocal = mode == "local";
// 判断是否是云上传
const isCloud = mode == "cloud";
// 获取文件路径
const filePath = file.path;
// 获取文件名
let fileName = file.name;
// 如果文件名不存在,则使用文件路径的文件名
if (fileName == "" || fileName == null) {
fileName = basename(filePath);
}
// 获取文件扩展名
let ext = extname(fileName);
if (ext == "") {
ext = "png";
}
// 生成唯一key: 原文件名_uuid.扩展名
let key = `${filename(fileName)}_${uuid()}.${ext}`;
// 云上传需要加上时间戳路径
if (isCloud) {
key = `app/${Date.now()}/${key}`;
}
// 支持多种上传方式
return new Promise((resolve, reject) => {
/**
* 实际上传文件的函数
* @param param0 上传参数
*/
function next({ url, preview, data }: UploadRequestOptions) {
// 发起上传请求
const task = uni.uploadFile({
url,
filePath,
name: "file",
header: {
// 本地上传带token
Authorization: isLocal ? user.token : null
},
formData: {
...(data as UTSJSONObject),
key
},
success(res) {
if (isLocal) {
// 本地上传返回处理
const { code, data, message } = parseObject<LocalUploadResponse>(res.data)!;
if (code == 1000) {
resolve(data);
} else {
reject(message);
}
} else {
// 云上传直接拼接url
resolve(pathJoin(preview ?? url, key!));
}
},
fail(err) {
console.error(err);
reject(err);
}
});
// 上传任务回调
if (options?.onTask != null) {
options.onTask!({
abort: () => {
task.abort();
}
} as UploadTask);
}
// 上传进度回调
if (options?.onProgressUpdate != null) {
task.onProgressUpdate((result) => {
const { progress, totalBytesSent, totalBytesExpectedToSend } = result;
options.onProgressUpdate!({
progress,
totalBytesSent,
totalBytesExpectedToSend
});
});
}
}
// 本地上传
if (isLocal) {
next({
url: config.baseUrl + "/app/base/comm/upload",
data: {}
});
} else {
// 云上传
const data = {} as UTSJSONObject;
// AWS需要提前传key
if (type == "aws") {
data.key = key;
}
// 获取云上传参数
request({
url: "/app/base/comm/upload",
method: "POST",
data
})
.then((res) => {
const d = parse<CloudUploadResponse>(res!)!;
switch (type) {
// 腾讯云COS
case "cos":
next({
url: d.url!,
data: d.credentials!
});
break;
// 阿里云OSS
case "oss":
next({
url: d.host!,
data: {
OSSAccessKeyId: d.OSSAccessKeyId,
policy: d.policy,
signature: d.signature
}
});
break;
// 七牛云
case "qiniu":
next({
url: d.uploadUrl!,
preview: d.publicDomain,
data: {
token: d.token
}
});
break;
// 亚马逊AWS
case "aws":
next({
url: d.url!,
data: d.fields!
});
break;
}
})
.catch(reject);
}
});
}

View File

@@ -0,0 +1,612 @@
/**
* 检查值是否为数组
* @example isArray([1, 2, 3]) // true
* @example isArray('123') // false
*/
export function isArray(value: any): boolean {
return Array.isArray(value);
}
/**
* 检查值是否为对象
* @example isObject({}) // true
* @example isObject([]) // false
*/
export function isObject(value: any): boolean {
return typeof value == "object" && !Array.isArray(value) && !isNull(value);
}
/**
* 检查值是否为字符串
* @example isString('abc') // true
* @example isString(123) // false
*/
export function isString(value: any): boolean {
return typeof value == "string";
}
/**
* 检查值是否为数字
* @example isNumber(123) // true
* @example isNumber('123') // false
*/
export function isNumber(value: any): boolean {
return typeof value == "number" && !isNaN(value);
}
/**
* 检查值是否为布尔值
* @example isBoolean(true) // true
* @example isBoolean(1) // false
*/
export function isBoolean(value: any): boolean {
return typeof value == "boolean";
}
/**
* 检查值是否为函数
* @example isFunction(() => {}) // true
* @example isFunction({}) // false
*/
export function isFunction(value: any): boolean {
return typeof value == "function";
}
/**
* 检查值是否为 null
* @example isNull(null) // true
* @example isNull(undefined) // true
*/
export function isNull(value?: any | null): boolean {
// #ifdef APP
return value == null;
// #endif
// #ifndef APP
return value == null || value == undefined;
// #endif
}
/**
* 检查值是否为空
* @example isEmpty([]) // true
* @example isEmpty('') // true
* @example isEmpty({}) // true
*/
export function isEmpty(value: any): boolean {
if (isArray(value)) {
return (value as any[]).length == 0;
}
if (isString(value)) {
return value == "";
}
if (isObject(value)) {
return keys(value).length == 0;
}
return false;
}
/**
* 返回对象的所有键名
* @example keys({a: 1, b: 2}) // ['a', 'b']
*/
export function keys(value: any): string[] {
// @ts-ignore
return UTSJSONObject.keys(value as UTSJSONObject);
}
/**
* 返回数组的第一个元素
* @example first([1, 2, 3]) // 1
* @example first([]) // null
*/
export function first<T>(array: T[]): T | null {
return isArray(array) && array.length > 0 ? array[0] : null;
}
/**
* 返回数组的最后一个元素
* @example last([1, 2, 3]) // 3
* @example last([]) // null
*/
export function last<T>(array: T[]): T | null {
return isArray(array) && array.length > 0 ? array[array.length - 1] : null;
}
/**
* 截取数组的一部分
* @example slice([1, 2, 3], 1) // [2, 3]
* @example slice([1, 2, 3], 1, 2) // [2]
*/
export function slice<T>(array: T[], start: number = 0, end: number = array.length): T[] {
if (!isArray(array)) return [];
const result: T[] = [];
for (let i = start; i < end && i < array.length; i++) {
result.push(array[i]);
}
return result;
}
/**
* 检查对象是否包含指定属性
* @example has({a: 1}, 'a') // true
* @example has({a: 1}, 'b') // false
*/
export function has(object: any, key: string): boolean {
return keys(object).includes(key);
}
/**
* 获取对象的属性值
* @example get({a: {b: 1}}, 'a.b') // 1
* @example get({a: {b: 1}}, 'a.c', 'default') // 'default'
*/
export function get(object: any, path: string, defaultValue: any | null = null): any | null {
if (isNull(object)) {
return defaultValue;
}
// @ts-ignore
const value = new UTSJSONObject(object).getAny(path);
if (isNull(value)) {
return defaultValue;
}
return value;
}
/**
* 设置对象的属性值
* @example set({a: 1}, 'b', 2) // {a: 1, b: 2}
*/
export function set(object: any, key: string, value: any | null): void {
(object as UTSJSONObject)[key] = value;
}
/**
* 遍历数组并返回新数组
* @example map([1, 2, 3], x => x * 2) // [2, 4, 6]
*/
export function map<T, U>(array: T[], iteratee: (item: T, index: number) => U): U[] {
const result: U[] = [];
if (!isArray(array)) return result;
for (let i = 0; i < array.length; i++) {
result.push(iteratee(array[i], i));
}
return result;
}
/**
* 将数组归约为单个值
* @example reduce([1, 2, 3], (sum, n) => sum + n, 0) // 6
*/
export function reduce<T, U>(
array: T[],
iteratee: (accumulator: U, value: T, index: number) => U,
initialValue: U
): U {
if (!isArray(array)) return initialValue;
let accumulator: U = initialValue;
for (let i = 0; i < array.length; i++) {
accumulator = iteratee(accumulator, array[i], i);
}
return accumulator;
}
/**
* 检查数组中的所有元素是否都满足条件
* @example every([2, 4, 6], x => x % 2 == 0) // true
*/
export function every<T>(array: T[], predicate: (value: T, index: number) => boolean): boolean {
if (!isArray(array)) return true;
for (let i = 0; i < array.length; i++) {
if (!predicate(array[i], i)) {
return false;
}
}
return true;
}
/**
* 检查数组中是否有元素满足条件
* @example some([1, 2, 3], x => x > 2) // true
*/
export function some<T>(array: T[], predicate: (value: T, index: number) => boolean): boolean {
if (!isArray(array)) return false;
for (let i = 0; i < array.length; i++) {
if (predicate(array[i], i)) {
return true;
}
}
return false;
}
/**
* 创建去重后的数组
* @example uniq([1, 2, 2, 3]) // [1, 2, 3]
*/
export function uniq<T>(array: T[]): T[] {
if (!isArray(array)) return [];
const result: T[] = [];
const seen = new Map<T, boolean>();
for (let i = 0; i < array.length; i++) {
const item = array[i];
const key = item;
if (!seen.has(item)) {
seen.set(key, true);
result.push(item);
}
}
return result;
}
/**
* 将数组扁平化一层
* @example flatten([1, [2, 3], 4]) // [1, 2, 3, 4]
*/
export function flatten(array: any[]): any[] {
if (!isArray(array)) return [];
const result: any[] = [];
for (let i = 0; i < array.length; i++) {
const item = array[i];
if (isArray(item)) {
result.push(...(item as any[]));
} else {
result.push(item);
}
}
return result;
}
/**
* 将数组完全扁平化
* @example flattenDeep([1, [2, [3, [4]]]]) // [1, 2, 3, 4]
*/
export function flattenDeep(array: any[]): any[] {
if (!isArray(array)) return [];
const result: any[] = [];
for (let i = 0; i < array.length; i++) {
const item = array[i];
if (isArray(item)) {
result.push(...flattenDeep(item as any[]));
} else {
result.push(item);
}
}
return result;
}
/**
* 对数组进行排序
* @example sort([3, 1, 2]) // [1, 2, 3]
* @example sort(['c', 'a', 'b'], 'desc') // ['c', 'b', 'a']
*/
export function sort<T>(array: T[], order: "asc" | "desc" = "asc"): T[] {
const result = [...array];
return result.sort((a, b) => {
if (typeof a == "number" && typeof b == "number") {
return order == "asc" ? a - b : b - a;
}
if (typeof a == "string" && typeof b == "string") {
return order == "asc" ? a.localeCompare(b) : b.localeCompare(a);
}
return 0;
});
}
/**
* 根据对象属性对数组进行排序
* @example orderBy([{age: 30}, {age: 20}], 'age') // [{age: 20}, {age: 30}]
*/
export function orderBy<T>(array: T[], key: string, order: "asc" | "desc" = "asc"): T[] {
if (!isArray(array)) return [];
const result = [...array];
result.sort((a, b) => {
const valueA = get(a as any, key) as number;
const valueB = get(b as any, key) as number;
if (order == "asc") {
return valueA > valueB ? 1 : -1;
} else {
return valueA < valueB ? 1 : -1;
}
});
return result;
}
/**
* 根据对象属性对数组进行分组
* @example groupBy([{type: 'a'}, {type: 'b'}, {type: 'a'}], 'type') // {a: [{type: 'a'}, {type: 'a'}], b: [{type: 'b'}]}
*/
export function groupBy<T>(array: T[], key: string) {
if (!isArray(array)) return {};
const result = {};
for (let i = 0; i < array.length; i++) {
const item = array[i];
let value = get(item as any, key)!;
if (typeof value == "number") {
value = value.toString();
}
if (typeof value == "string") {
if (!isArray(result[value])) {
result[value] = new Array<T>();
}
(result[value] as T[]).push(item);
}
}
return result;
}
/**
* 将多个对象的属性合并到一个对象中
* @example assign({a: 1}, {b: 2}) // {a: 1, b: 2}
*/
export function assign(...items: any[]) {
// @ts-ignore
return UTSJSONObject.assign(...items.map((item) => item as UTSJSONObject));
}
/**
* 获取数组中指定索引的元素
* @example nth([1, 2, 3], 1) // 2
* @example nth([1, 2, 3], -1) // 3
*/
export function nth<T>(array: T[], index: number): T | null {
if (index >= 0) {
return array[index];
}
return array[array.length + index];
}
/**
* 从数组中移除指定的值
* @example pull([1, 2, 3, 1, 2, 3], 1, 2) // [3, 3]
*/
export function pull<T>(array: T[], ...values: T[]): T[] {
if (!isArray(array)) return [];
return array.filter((item) => !values.includes(item));
}
/**
* 从数组中移除满足条件的元素
* @example remove([1, 2, 3, 4], x => x % 2 == 0) // [1, 3]
*/
export function remove<T>(array: T[], predicate: (value: T, index: number) => boolean): T[] {
if (!isArray(array)) return [];
const result: T[] = [];
for (let i = 0; i < array.length; i++) {
if (!predicate(array[i], i)) {
result.push(array[i]);
}
}
return result;
}
/**
* 遍历数组
* @example forEach([1, 2, 3], x => console.log(x))
*/
export function forEach<T>(data: T[], iteratee: (value: T, index: number) => void): void {
if (isArray(data)) {
const array = data as T[];
for (let i = 0; i < array.length; i++) {
if (array[i] != null) {
iteratee(array[i], i);
}
}
}
}
/**
* 查找数组中第一个满足条件的元素
* @example find([1, 2, 3, 4], x => x > 2) // 3
*/
export function find<T>(array: T[], predicate: (value: T, index: number) => boolean): T | null {
if (!isArray(array)) return null;
for (let i = 0; i < array.length; i++) {
if (predicate(array[i], i)) {
return array[i];
}
}
return null;
}
/**
* 遍历对象
* @example forInObject({a: 1, b: 2}, (value, key) => console.log(key, value))
*/
export function forInObject(data: any, iteratee: (value: any, key: string) => void): void {
if (isObject(data)) {
const objKeys = keys(data);
for (let i = 0; i < objKeys.length; i++) {
const key = objKeys[i];
iteratee(get(data, key)!, key);
}
}
}
/**
* 对象转数组
* @example toArray({a: 1, b: 2}, (value, key) => ({key, value})) // [{key: 'a', value: 1}, {key: 'b', value: 2}]
*/
export function toArray<T>(data: any, iteratee: (value: any, key: string) => T): T[] {
const result: T[] = [];
if (isObject(data)) {
forInObject(data, (value, key) => {
result.push(iteratee(value, key));
});
}
return result;
}
/**
* 生成UUID
* @example uuid() // "123e4567-e89b-12d3-a456-426614174000"
*/
export function uuid(): string {
let uuid = "";
let i: number;
let random: number;
for (i = 0; i < 36; i++) {
if (i == 8 || i == 13 || i == 18 || i == 23) {
uuid += "-";
} else if (i == 14) {
uuid += "4";
} else if (i == 19) {
random = (Math.random() * 16) | 0;
uuid += ((random & 0x3) | 0x8).toString(16);
} else {
random = (Math.random() * 16) | 0;
uuid += random.toString(16);
}
}
return uuid;
}
/**
* 创建一个防抖函数,在指定延迟后执行函数,如果在延迟期间再次调用则重新计时
* @example debounce(() => console.log('执行'), 300)
*/
export function debounce(func: () => void, delay: number): () => number {
let timeoutId = 0;
return function (): number {
// 清除之前的定时器
if (timeoutId != 0) {
clearTimeout(timeoutId);
}
// 设置新的定时器
// @ts-ignore
timeoutId = setTimeout(() => {
func();
timeoutId = 0;
}, delay);
return timeoutId;
};
}
/**
* 创建一个节流函数,在指定时间间隔内只会执行一次
* @example
* const throttled = throttle(() => console.log('执行'), 300)
* throttled()
*/
export function throttle(func: () => void, delay: number): () => number {
let timeoutId: number = 0;
let lastExec: number = 0;
return function (): number {
const now: number = Date.now();
// 如果距离上次执行已超过delay则立即执行
if (now - lastExec >= delay) {
func();
lastExec = now;
return 0;
}
// 否则在剩余时间后执行
if (timeoutId != 0) {
clearTimeout(timeoutId);
}
const remaining: number = delay - (now - lastExec);
// @ts-ignore
timeoutId = setTimeout(() => {
func();
lastExec = Date.now();
timeoutId = 0;
}, remaining);
return timeoutId;
};
}
/**
* 生成指定范围内的随机数
* @example random(1, 10) // 随机生成1到10之间的整数
*/
export function random(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
/**
* 将base64转换为blob
* @param data base64数据
* @returns blob数据
*/
export function base64ToBlob(data: string, type: string = "image/jpeg"): Blob {
// #ifdef H5
let bytes = window.atob(data.split(",")[1]);
let ab = new ArrayBuffer(bytes.length);
let ia = new Uint8Array(ab);
for (let i = 0; i < bytes.length; i++) {
ia[i] = bytes.charCodeAt(i);
}
return new Blob([ab], { type });
// #endif
}
/**
* 检查两个值是否相等
* @param a 值1
* @param b 值2
* @returns 是否相等
*/
export function isEqual(a: any, b: any): boolean {
if (isObject(a) && isObject(b)) {
return isEqual(JSON.stringify(a), JSON.stringify(b));
} else if (isArray(a) && isArray(b)) {
return isEqual(JSON.stringify(a), JSON.stringify(b));
}
return a == b;
}

324
cool-unix/cool/utils/day.ts Normal file
View File

@@ -0,0 +1,324 @@
/**
* 轻量级日期工具类
* 基于 uni-app-x UTS Date API
* 参考 dayjs 设计理念
*/
export class DayUts {
private _date: Date;
constructor(date?: Date | string | number | null) {
if (date == null || date == "") {
this._date = new Date();
} else if (typeof date == "string") {
this._date = new Date(date);
} else if (typeof date == "number") {
this._date = new Date(date);
} else if (date instanceof Date) {
this._date = new Date(date.getTime());
} else {
this._date = new Date();
}
}
/**
* 格式化日期
* @param template 格式模板,支持 YYYY-MM-DD HH:mm:ss 等
*/
format(template: string): string {
// 使用传入的模板
let actualTemplate: string = template;
const year = this._date.getFullYear();
const month = this._date.getMonth() + 1;
const date = this._date.getDate();
const hours = this._date.getHours();
const minutes = this._date.getMinutes();
const seconds = this._date.getSeconds();
const milliseconds = this._date.getMilliseconds();
// 使用数组来依次替换避免UTS的replace方法兼容性问题
let result: string = actualTemplate;
// 按照长度从长到短替换,避免冲突
// 替换年份 (YYYY 必须在 YY 之前)
result = result.replace("YYYY", year.toString());
result = result.replace("YY", year.toString().slice(-2));
// 替换月份 (MM 必须在 M 之前)
result = result.replace("MM", month.toString().padStart(2, "0"));
result = result.replace("M", month.toString());
// 替换日期 (DD 必须在 D 之前)
result = result.replace("DD", date.toString().padStart(2, "0"));
result = result.replace("D", date.toString());
// 替换小时 (HH 必须在 H 之前)
result = result.replace("HH", hours.toString().padStart(2, "0"));
result = result.replace("H", hours.toString());
// 替换分钟 (mm 必须在 m 之前)
result = result.replace("mm", minutes.toString().padStart(2, "0"));
result = result.replace("m", minutes.toString());
// 替换秒数 (ss 必须在 s 之前)
result = result.replace("ss", seconds.toString().padStart(2, "0"));
result = result.replace("s", seconds.toString());
// 替换毫秒
result = result.replace("SSS", milliseconds.toString().padStart(3, "0"));
return result;
}
/**
* 本月多少天
*/
getDays(): number {
return new Date(this._date.getFullYear(), this._date.getMonth() + 1, 0).getDate();
}
/**
* 是否为闰年
*/
isLeapYear(): boolean {
return this._date.getFullYear() % 4 == 0 && this._date.getFullYear() % 100 != 0;
}
/**
* 星期几
*/
getDay(): number {
return this._date.getDay();
}
/**
* 获取某个单位的开始时间
*/
startOf(unit: "month" | "day" | "year" | "week"): DayUts {
const newDate = new Date(this._date.getTime());
switch (unit) {
case "year":
newDate.setMonth(0);
newDate.setDate(1);
newDate.setHours(0);
newDate.setMinutes(0);
newDate.setSeconds(0);
newDate.setMilliseconds(0);
break;
case "month":
newDate.setDate(1);
newDate.setHours(0);
newDate.setMinutes(0);
newDate.setSeconds(0);
newDate.setMilliseconds(0);
break;
case "week":
newDate.setDate(newDate.getDate() - newDate.getDay());
newDate.setHours(0);
newDate.setMinutes(0);
newDate.setSeconds(0);
newDate.setMilliseconds(0);
break;
case "day":
newDate.setHours(0);
newDate.setMinutes(0);
newDate.setSeconds(0);
newDate.setMilliseconds(0);
break;
}
return new DayUts(newDate);
}
/**
* 获取某个单位的结束时间
*/
endOf(unit: "month" | "day" | "year" | "week"): DayUts {
const newDate = new Date(this._date.getTime());
switch (unit) {
case "year":
newDate.setMonth(11);
newDate.setDate(31);
newDate.setHours(23);
newDate.setMinutes(59);
newDate.setSeconds(59);
newDate.setMilliseconds(999);
break;
case "month":
newDate.setMonth(newDate.getMonth() + 1);
newDate.setDate(0);
newDate.setHours(23);
newDate.setMinutes(59);
newDate.setSeconds(59);
newDate.setMilliseconds(999);
break;
case "week":
const day = newDate.getDay();
const diff = 6 - day;
newDate.setDate(newDate.getDate() + diff);
newDate.setHours(23);
newDate.setMinutes(59);
newDate.setSeconds(59);
newDate.setMilliseconds(999);
break;
case "day":
newDate.setHours(23);
newDate.setMinutes(59);
newDate.setSeconds(59);
newDate.setMilliseconds(999);
break;
}
return new DayUts(newDate);
}
/**
* 判断是否早于另一个日期
*/
isBefore(date: DayUts | Date | string | number): boolean {
const compareDate = this._parseDate(date);
return this._date.getTime() < compareDate.getTime();
}
/**
* 判断是否晚于另一个日期
*/
isAfter(date: DayUts | Date | string | number): boolean {
const compareDate = this._parseDate(date);
return this._date.getTime() > compareDate.getTime();
}
/**
* 判断是否与另一个日期相同
*/
isSame(date: DayUts | Date | string | number): boolean {
const compareDate = this._parseDate(date);
return this._date.getTime() == compareDate.getTime();
}
/**
* 计算与另一个日期的差值(毫秒)
*/
diff(date: DayUts | Date | string | number): number {
const compareDate = this._parseDate(date);
return this._date.getTime() - compareDate.getTime();
}
/**
* 计算与另一个日期的差值(指定单位)
*/
diffUnit(
date: DayUts | Date | string | number,
unit: "day" | "hour" | "minute" | "second" | "millisecond"
): number {
const compareDate = this._parseDate(date);
const diffMs = this._date.getTime() - compareDate.getTime();
switch (unit) {
case "day":
return Math.floor(diffMs / (1000 * 60 * 60 * 24));
case "hour":
return Math.floor(diffMs / (1000 * 60 * 60));
case "minute":
return Math.floor(diffMs / (1000 * 60));
case "second":
return Math.floor(diffMs / 1000);
case "millisecond":
default:
return diffMs;
}
}
/**
* 添加时间
*/
add(value: number, unit: "day" | "month" | "year" | "hour" | "minute" | "second"): DayUts {
const newDate = new Date(this._date.getTime());
switch (unit) {
case "year":
newDate.setFullYear(newDate.getFullYear() + value);
break;
case "month":
newDate.setMonth(newDate.getMonth() + value);
break;
case "day":
newDate.setDate(newDate.getDate() + value);
break;
case "hour":
newDate.setHours(newDate.getHours() + value);
break;
case "minute":
newDate.setMinutes(newDate.getMinutes() + value);
break;
case "second":
newDate.setSeconds(newDate.getSeconds() + value);
break;
}
return new DayUts(newDate);
}
/**
* 减少时间
*/
subtract(value: number, unit: "day" | "month" | "year" | "hour" | "minute" | "second"): DayUts {
return this.add(-value, unit);
}
/**
* 获取时间戳
*/
valueOf(): number {
return this._date.getTime();
}
/**
* 获取原生Date对象
*/
toDate(): Date {
return new Date(this._date.getTime());
}
/**
* 获取日期数组
*/
toArray(): number[] {
return [
this._date.getFullYear(),
this._date.getMonth() + 1,
this._date.getDate(),
this._date.getHours(),
this._date.getMinutes(),
this._date.getSeconds()
];
}
/**
* 私有方法:解析不同类型的日期参数
*/
private _parseDate(date: DayUts | Date | string | number): Date {
if (date instanceof DayUts) {
return date.toDate();
} else if (date instanceof Date) {
return date;
} else if (typeof date == "string") {
return new Date(date);
} else if (typeof date == "number") {
return new Date(date);
} else {
// 如果都不匹配,返回当前时间
return new Date();
}
}
}
/**
* 创建 DayUts 实例
*/
export function dayUts(date: Date | string | number | null = new Date()): DayUts {
return new DayUts(date);
}

View File

@@ -0,0 +1,84 @@
/**
* 检查是否为小程序环境
* @returns 是否为小程序环境
*/
export const isMp = (): boolean => {
// #ifdef MP
return true;
// #endif
return false;
};
/**
* 检查是否为App环境
* @returns 是否为App环境
*/
export const isApp = (): boolean => {
// #ifdef APP
return true;
// #endif
return false;
};
/**
* 检查是否为App-IOS环境
* @returns 是否为App-IOS环境
*/
export const isAppIOS = (): boolean => {
// #ifdef APP-IOS
return true;
// #endif
return false;
};
/**
* 检查是否为App-Android环境
* @returns 是否为App-Android环境
*/
export const isAppAndroid = (): boolean => {
// #ifdef APP-ANDROID
return true;
// #endif
return false;
};
/**
* 检查是否为H5环境
* @returns 是否为H5环境
*/
export const isH5 = (): boolean => {
// #ifdef H5
return true;
// #endif
return false;
};
/**
* 检查是否为鸿蒙环境
* @returns 是否为鸿蒙环境
*/
export const isHarmony = (): boolean => {
// #ifdef APP-HARMONY
return true;
// #endif
return false;
};
/**
* 获取设备像素比
* @returns 设备像素比
*/
export const getDevicePixelRatio = (): number => {
const dpr = uni.getDeviceInfo().devicePixelRatio ?? 1;
// #ifdef MP
// 微信小程序高清处理
return 3;
// #endif
return dpr;
};

View File

@@ -0,0 +1,74 @@
import { base64ToBlob, uuid } from "./comm";
/**
* 将canvas转换为png图片
* @param options 转换参数
* @returns 图片路径
*/
export function canvasToPng(canvasRef: UniElement): Promise<string> {
return new Promise((resolve) => {
// #ifdef APP
canvasRef.parentElement!.takeSnapshot({
success(res) {
resolve(res.tempFilePath);
},
fail(err) {
console.error(err);
resolve("");
}
});
// #endif
// #ifdef H5
const url = URL.createObjectURL(
base64ToBlob(
(canvasRef as unknown as HTMLCanvasElement)?.toDataURL("image/png", 1) ?? ""
)
);
resolve(url);
// #endif
// #ifdef MP
uni.createCanvasContextAsync({
id: canvasRef.id,
component: canvasRef.$vm,
success(context) {
// 获取2D绘图上下文
const ctx = context.getContext("2d")!;
// 获取canvas对象
const canvas = ctx.canvas;
// 将canvas转换为base64格式的PNG图片数据
const data = canvas.toDataURL("image/png", 1);
// 获取文件系统管理器
const fileMg = uni.getFileSystemManager();
// 生成临时文件路径
// @ts-ignore
const filepath = `${wx.env.USER_DATA_PATH}/${uuid()}.png`;
// 将base64数据写入文件
fileMg.writeFile({
filePath: filepath,
data: data.split(",")[1],
encoding: "base64",
success() {
resolve(filepath);
},
fail(err) {
console.error(err);
resolve("");
}
});
},
fail(err) {
console.error(err);
resolve("");
}
});
// #endif
});
}

View File

@@ -0,0 +1,9 @@
export * from "./tailwind";
export * from "./comm";
export * from "./day";
export * from "./device";
export * from "./file";
export * from "./parse";
export * from "./path";
export * from "./rect";
export * from "./storage";

View File

@@ -0,0 +1,244 @@
import { forEach, forInObject, isArray, isObject, isString } from "./comm";
/**
* 解析数据
* @example parse<Response>(res.data)
*/
export function parse<T>(data: any): T | null {
// #ifdef APP-ANDROID
// @ts-ignore
return (data as UTSJSONObject).parse<T>();
// #endif
// #ifndef APP-ANDROID
return data as T;
// #endif
}
/**
* 解析JSON对象
* @param data 要解析的数据
* @returns 解析后的JSON对象
*/
export function parseObject<T>(data: string): T | null {
// #ifdef APP-ANDROID
return JSON.parseObject<T>(data);
// #endif
// #ifndef APP-ANDROID
return JSON.parse(data) as T;
// #endif
}
/**
* 解析透传样式对象
* @param data 要解析的数据
* @returns 解析后的透传样式对象
* @template T 透传样式对象的类型
*/
export function parsePt<T>(data: any): T {
// #ifdef APP-ANDROID
// @ts-ignore
return (data as UTSJSONObject).parse<T>() ?? ({} as T);
// #endif
// #ifndef APP-ANDROID
return data as T;
// #endif
}
/**
* 解析对象为类名字符串
* @param obj 要解析的对象,key为类名,value为布尔值表示是否启用该类名
* @returns 解析后的类名字符串,多个类名以空格分隔
* @example
* parseClass({ 'active': true, 'disabled': false }) // 返回 'active'
* parseClass(['ml-2', 'mr-2']) // 返回 'ml-2 mr-2'
* parseClass([{ 'mr-2': true, 'mt-2': false }]) // 返回 'mr-2'
* parseClass([[true, 'mr-2 pt-2', 'mb-2']]) // 返回 'mr-2 pt-2'
*/
export const parseClass = (data: any): string => {
// 存储启用的类名
const names: string[] = [];
// 解析数据
function deep(d: any) {
// 如果obj是数组,则将数组中的每个元素添加到names中
if (isArray(d)) {
forEach(d as any[], (value: any) => {
if (isString(value)) {
// @example 2
names.push(value as string);
} else if (isArray(value)) {
// @example 4
const [a, b] = value as any[];
if (a as boolean) {
names.push(b as string);
} else {
if (value.length > 2) {
names.push(value[2] as string);
}
}
} else if (isObject(value)) {
// @example 3
deep(value);
}
});
}
// 遍历对象的每个属性
if (isObject(d)) {
// @example 1
forInObject(d, (value, key) => {
// 如果属性值为true,则将类名添加到数组中
if (value == true && key != "") {
names.push(key.trim());
}
});
}
}
deep(data);
// 将类名数组用空格连接成字符串返回
return names.join(" ");
};
/**
* 将自定义类型数据转换为UTSJSONObject对象
* @param data 要转换的数据
* @returns 转换后的UTSJSONObject对象
*/
export function parseToObject<T>(data: T): UTSJSONObject {
// #ifdef APP-ANDROID
return JSON.parseObject(JSON.stringify(data ?? {})!)!;
// #endif
// #ifndef APP-ANDROID
return JSON.parse(JSON.stringify(data || {})) as UTSJSONObject;
// #endif
}
/**
* 将rpx单位转换为px单位
* @param rpx 要转换的rpx值
* @returns 转换后的px值
* @example
*/
export const rpx2px = (rpx: number): number => {
let px: number;
// #ifdef MP
px = rpx / (750 / uni.getWindowInfo().windowWidth);
// #endif
// #ifndef MP
px = uni.rpx2px(rpx);
// #endif
return px;
};
/**
* 将px单位转换为rpx单位
* @param px 要转换的px值
* @returns 转换后的rpx值
* @example
*/
export const px2rpx = (px: number): number => {
return px / rpx2px(1);
};
/**
* 将数值或字符串转换为rpx单位的字符串
* @param val 要转换的值,可以是数字或字符串
* @returns 转换后的rpx单位字符串
* @example
* parseRpx(10) // 返回 '10rpx'
* parseRpx('10rpx') // 返回 '10rpx'
* parseRpx('10px') // 返回 '10px'
*/
export const parseRpx = (val: number | string): string => {
if (typeof val == "number") {
return val + "rpx";
}
return val;
};
/**
* 示例: 获取数值部分
* @example
* getNum("10rpx") // 返回 10
* getNum("10px") // 返回 10
* getNum("10") // 返回 10
* getNum("-5.5px") // 返回 -5.5
* @param val - 输入值,例如 "10rpx"、"10px"、"10"
* @returns number - 返回提取的数值
*/
export const getNum = (val: string): number => {
// 使用正则提取数字部分,支持小数和负数
const match = val.match(/-?\d+(\.\d+)?/);
return match != null ? parseFloat(match[0] ?? "0") : 0;
};
/**
* 示例: 获取单位
* @example
* getUnit("10rpx") // 返回 "rpx"
* getUnit("10px") // 返回 "px"
* @param val - 输入值,例如 "10rpx"、"10px"
* @returns string - 返回单位字符串,如 "rpx" 或 "px"
*/
export const getUnit = (val: string): string => {
const num = getNum(val);
return val.replace(`${num}`, "");
};
/**
* 示例: 转换为 rpx 值
* @example
* getRpx("10rpx") // 返回 10
* getRpx("10px") // 返回 px2rpx(10)
* getRpx(10) // 返回 10
* @param val - 输入值,可以是 "10rpx"、"10px" 或数字 10
* @returns number - 返回对应的 rpx 数值
*/
export const getRpx = (val: number | string): number => {
if (typeof val == "number") {
return val;
}
const num = getNum(val);
const unit = getUnit(val);
if (unit == "px") {
return px2rpx(num);
}
return num;
};
/**
* 示例: 转换为 px 值
* @example
* getPx("10rpx") // 返回 rpx2px(10)
* getPx("10px") // 返回 10
* getPx(10) // 返回 rpx2px(10)
* @param val - 输入值,可以是 "10rpx"、"10px" 或数字 10
* @returns number - 返回对应的 px 数值
*/
export const getPx = (val: string | number) => {
if (typeof val == "number") {
return rpx2px(val);
}
const num = getNum(val);
const unit = getUnit(val);
if (unit == "rpx") {
return rpx2px(num);
}
return num;
};

View File

@@ -0,0 +1,60 @@
/**
* 获取文件名
* @example filename("a/b/c.txt") // "c"
*/
export function filename(path: string): string {
return basename(path.substring(0, path.lastIndexOf(".")));
}
/**
* 获取路径的最后一部分
* @example basename("a/b/c.txt") // "c.txt"
*/
export function basename(path: string): string {
let index = path.lastIndexOf("/");
index = index > -1 ? index : path.lastIndexOf("\\");
if (index < 0) {
return path;
}
return path.substring(index + 1);
}
/**
* 获取文件扩展名
* @example extname("a/b/c.txt") // "txt"
*/
export function extname(path: string): string {
let index = path.lastIndexOf(".");
if (index < 0) {
return "";
}
return path.substring(index + 1);
}
/**
* 首字母大写
* @example firstUpperCase("useInfo") // "UseInfo"
*/
export function firstUpperCase(value: string): string {
return value.charAt(0).toLocaleUpperCase() + value.slice(1);
}
/**
* 获取地址栏参数
* @example getUrlParam("a") // "1"
*/
export function getUrlParam(name: string): string | null {
// #ifdef H5
const params = new URLSearchParams(window.location.search);
const value = params.get(name);
return value !== null ? decodeURIComponent(value) : null;
// #endif
}
/**
* 连接路径
* @example pathJoin("https://www.baidu.com/", "/a/b/c.txt") // "https://www.baidu.com/a/b/c.txt"
*/
export function pathJoin(...parts: string[]): string {
return parts.map((part) => part.replace(/(^\/+|\/+$)/g, "")).join("/");
}

View File

@@ -0,0 +1,68 @@
import { config } from "@/config";
import { router } from "../router";
import { isH5, isHarmony } from "./device";
import { ctx } from "../ctx";
import { getPx } from "./parse";
/**
* 是否需要计算 tabBar 高度
* @returns boolean
*/
export function hasCustomTabBar() {
if (router.isTabPage()) {
if (isHarmony()) {
return false;
}
return config.isCustomTabBar || isH5();
}
return false;
}
/**
* 是否存在自定义 topbar
* @returns boolean
*/
export function hasCustomTopbar() {
return router.route()?.isCustomNavbar ?? false;
}
/**
* 获取安全区域高度
* @param type 类型
* @returns 安全区域高度
*/
export function getSafeAreaHeight(type: "top" | "bottom") {
const { safeAreaInsets } = uni.getWindowInfo();
let h: number;
if (type == "top") {
h = safeAreaInsets.top;
} else {
h = safeAreaInsets.bottom;
// #ifdef APP-ANDROID
if (h == 0) {
h = 16;
}
// #endif
}
return h;
}
/**
* 获取 tabBar 高度
* @returns tabBar 高度
*/
export function getTabBarHeight() {
let h = ctx.tabBar.height == null ? 50 : getPx(ctx.tabBar.height!);
if (hasCustomTabBar()) {
h += getSafeAreaHeight("bottom");
}
return h;
}

View File

@@ -0,0 +1,158 @@
// 过期时间后缀,用于标识存储数据的过期时间键名
const EXPIRES_SUFFIX = "_deadtime";
/**
* 存储管理类
*
* 封装了 uni-app 的存储 API提供更便捷的存储操作
* 支持数据过期时间管理,自动处理过期数据
*/
class Storage {
/**
* 获取存储数据
*
* @param key 存储键名
* @returns 存储的数据,如果不存在则返回 null
*
* @example
* const userData = storage.get('user');
* if (userData != null) {
* console.log(userData);
* }
*/
get(key: string): any | null {
return uni.getStorageSync(key);
}
/**
* 获取所有存储数据的信息
*
* 遍历所有存储键,返回包含所有键值对的对象
* 注意:此方法会读取所有存储数据,大量数据时需注意性能
*
* @returns 包含所有存储数据的对象
*
* @example
* const allData = storage.info();
* console.log('所有存储数据:', allData);
*/
info() {
// 获取存储信息,包含所有键名
const info = uni.getStorageInfoSync();
// 创建空对象用于存放所有数据
const d = {};
// 遍历所有键名,获取对应的值
info.keys.forEach((e) => {
d[e] = this.get(e);
});
return d;
}
/**
* 设置存储数据
*
* @param key 存储键名
* @param value 要存储的数据,支持任意类型
* @param expires 过期时间默认为0表示永不过期
*
* @example
* // 存储永久数据
* storage.set('user', { name: '张三', age: 25 }, 0);
*
* // 存储5分钟后过期的数据
* storage.set('token', 'abc123', 300);
*/
set(key: string, value: any, expires: number): void {
// 存储主要数据
uni.setStorageSync(key, value);
// 如果设置了过期时间,则存储过期时间戳
if (expires > 0) {
// 计算过期时间戳:当前时间 + 过期时间(秒转毫秒)
const expireTime = new Date().getTime() + expires * 1000;
uni.setStorageSync(`${key}${EXPIRES_SUFFIX}`, expireTime);
}
}
/**
* 检查数据是否已过期
*
* @param key 存储键名
* @returns true表示已过期或无过期时间设置false表示未过期
*
* @example
* if (storage.isExpired('token')) {
* console.log('token已过期');
* }
*/
isExpired(key: string): boolean {
// 获取过期时间戳
const value = uni.getStorageSync(`${key}${EXPIRES_SUFFIX}`) as number | null;
// 如果没有设置过期时间,视为已过期
if (value == null) {
return true;
}
// 比较过期时间戳与当前时间,判断是否过期
return value - new Date().getTime() <= 0;
}
/**
* 删除存储数据
*
* 会同时删除数据本身和对应的过期时间
*
* @param key 存储键名
*
* @example
* storage.remove('user');
* storage.remove('token');
*/
remove(key: string) {
// 删除主要数据
uni.removeStorageSync(key);
// 删除对应的过期时间数据
uni.removeStorageSync(`${key}${EXPIRES_SUFFIX}`);
}
/**
* 清空所有存储数据
*
* 警告:此操作会删除所有本地存储数据,请谨慎使用
*
* @example
* storage.clear(); // 清空所有数据
*/
clear() {
uni.clearStorageSync();
}
/**
* 获取数据后立即删除(一次性读取)
*
* 适用于临时数据、一次性令牌等场景
* 读取后数据会被自动删除,确保数据的一次性使用
*
* @param key 存储键名
* @returns 存储的数据,如果不存在则返回 null
*
* @example
* const tempToken = storage.once('temp_token');
* // tempToken 使用后,存储中的 temp_token 已被删除
*/
once(key: string): any | null {
// 先获取数据
const value = this.get(key);
// 立即删除数据
this.remove(key);
// 返回获取到的数据
return value;
}
}
// 导出存储实例,提供全局访问
export const storage = new Storage();

View File

@@ -0,0 +1,28 @@
/**
* 判断 Tailwind class 字符串中是否包含文本颜色类(如 text-red, text-red-500, text-sky 等)
* @param className 传入的 class 字符串
* @returns 是否包含文本颜色类
*/
export function hasTextColor(className: string): boolean {
if (className == "") return false;
const regex =
/\btext-(primary|surface|red|blue|green|yellow|purple|pink|indigo|gray|grey|black|white|orange|amber|lime|emerald|teal|cyan|sky|violet|fuchsia|rose|slate|zinc|neutral|stone)(?:-\d+)?\b/;
return regex.test(className);
}
/**
* 判断 Tailwind class 字符串中是否包含字体大小类
* 支持如 text-xs, text-sm, text-base, text-lg, text-xl, 以及 text-[22px]、text-[22rpx] 等自定义写法
* @param className 传入的 class 字符串
* @returns 是否包含字体大小类
*/
export function hasTextSize(className: string): boolean {
if (className == "") return false;
const regex =
/\btext-(xs|sm|md|base|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl|\[\d+[a-zA-Z%]*\])\b/;
return regex.test(className);
}

5294
cool-unix/data/pca.json Normal file

File diff suppressed because it is too large Load Diff

BIN
cool-unix/docs/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
cool-unix/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,35 @@
export const iconfont = {
"back": "e6db",
"yue": "e6bb",
"wancheng": "e6bc",
"shibai": "e6bd",
"bofang": "e6be",
"pinglun": "e6bf",
"huatong": "e6c0",
"dianzan": "e6c1",
"fuli": "e6c2",
"jiudian": "e6c3",
"tupian": "e6c5",
"dingwei": "e6c6",
"vip": "e6c8",
"yunduan": "e6c9",
"naozhong": "e6ca",
"jiaoliu": "e6cb",
"shouru": "e6cc",
"zhichu": "e6cd",
"shijian": "e6ce",
"paizhao": "e6cf",
"qiche": "e6d0",
"shuipiao": "e6d1",
"dingyue": "e6d2",
"kefu_2": "e6d3",
"tuichudenglu": "e6d4",
"pinglun_2": "e6d5",
"qianbao": "e6d6",
"sousuo_2": "e6d7",
"youhuiquan": "e6d8",
"gouwudai": "e6ea",
"guanli": "e6fd",
"qianbi": "e70d",
"huangguan": "e712"
};

View File

@@ -0,0 +1,2 @@
@import "./iconfont/index.scss";
@import "./remixicon/index.scss";

7
cool-unix/icons/index.ts Normal file
View File

@@ -0,0 +1,7 @@
import { iconfont } from "./iconfont";
import { remixicon } from "./remixicon";
export const icons = {
iconfont,
remixicon
};

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,798 @@
export const remixicon = {
"arrow-left-up-line": "ea66",
"arrow-up-line": "ea76",
"arrow-right-up-line": "ea70",
"arrow-right-line": "ea6c",
"arrow-right-down-line": "ea6a",
"arrow-down-line": "ea4c",
"arrow-left-down-line": "ea5e",
"arrow-left-line": "ea60",
"arrow-up-circle-line": "ea72",
"arrow-right-circle-line": "ea68",
"arrow-down-circle-line": "ea4a",
"arrow-left-circle-line": "ea5c",
"arrow-up-circle-fill": "ea71",
"arrow-right-circle-fill": "ea67",
"arrow-down-circle-fill": "ea49",
"arrow-left-circle-fill": "ea5b",
"arrow-up-s-line": "ea78",
"arrow-right-s-line": "ea6e",
"arrow-down-s-line": "ea4e",
"arrow-left-s-line": "ea64",
"arrow-left-s-fill": "ea63",
"arrow-down-s-fill": "ea4d",
"arrow-right-s-fill": "ea6d",
"arrow-up-s-fill": "ea77",
"arrow-up-down-line": "ea74",
"arrow-left-right-line": "ea62",
"arrow-right-double-line": "f2e5",
"arrow-up-double-line": "f2eb",
"skip-up-line": "f367",
"expand-up-down-line": "f327",
"expand-left-right-line": "f323",
"expand-left-line": "f321",
"expand-right-line": "f325",
"arrow-go-back-line": "ea58",
"arrow-go-forward-line": "ea5a",
"home-2-line": "ee19",
"home-2-fill": "ee18",
"store-2-line": "f1a5",
"store-2-fill": "f1a4",
"store-3-fill": "f1a6",
"store-3-line": "f1a7",
"ancient-pavilion-line": "ea34",
"ancient-pavilion-fill": "ea33",
"tent-line": "f3df",
"tent-fill": "f3de",
"hospital-fill": "ee36",
"hospital-line": "ee37",
"ancient-gate-line": "ea32",
"ancient-gate-fill": "ea31",
"mail-line": "eef6",
"mail-fill": "eef3",
"mail-send-line": "eefc",
"mail-send-fill": "eefb",
"mail-unread-fill": "ef01",
"mail-unread-line": "ef02",
"mail-add-fill": "eeeb",
"mail-add-line": "eeec",
"mail-ai-line": "f585",
"mail-ai-fill": "f584",
"inbox-line": "ee4f",
"inbox-fill": "ee4e",
"inbox-archive-line": "ee4d",
"inbox-archive-fill": "ee4c",
"inbox-unarchive-line": "ee51",
"inbox-unarchive-fill": "ee50",
"cloud-line": "eb9d",
"cloud-fill": "eb9c",
"cloud-off-line": "eb9f",
"cloud-off-fill": "eb9e",
"archive-line": "ea48",
"archive-fill": "ea47",
"profile-fill": "f02c",
"profile-line": "f02d",
"award-line": "ea8a",
"award-fill": "ea89",
"at-line": "ea83",
"at-fill": "ea82",
"medal-fill": "ef27",
"medal-line": "ef28",
"verified-badge-line": "f3e9",
"verified-badge-fill": "f3e8",
"bar-chart-line": "ea9e",
"bar-chart-horizontal-line": "ea9d",
"bar-chart-2-line": "ea96",
"pie-chart-line": "effa",
"bubble-chart-line": "eb03",
"bar-chart-grouped-line": "ea9b",
"donut-chart-line": "ec42",
"line-chart-line": "eeab",
"bookmark-fill": "eae4",
"bookmark-line": "eae5",
"briefcase-fill": "eaf4",
"briefcase-line": "eaf5",
"donut-chart-fill": "ec41",
"line-chart-fill": "eeaa",
"calendar-line": "eb27",
"calendar-fill": "eb26",
"calculator-fill": "eb1e",
"calculator-line": "eb1f",
"customer-service-line": "ec0e",
"customer-service-fill": "ec0d",
"flag-fill": "ed3a",
"flag-line": "ed3b",
"flag-off-line": "f577",
"flag-off-fill": "f576",
"global-line": "edcf",
"global-fill": "edce",
"links-fill": "eeb7",
"links-line": "eeb8",
"printer-line": "f029",
"printer-fill": "f028",
"reply-line": "f07a",
"reply-fill": "f079",
"send-plane-line": "f0da",
"send-plane-fill": "f0d9",
"slideshow-fill": "f157",
"slideshow-line": "f158",
"window-line": "f2c6",
"window-fill": "f2c5",
"stack-fill": "f180",
"stack-line": "f181",
"service-fill": "f0e1",
"service-line": "f0e2",
"registered-fill": "f069",
"registered-line": "f06a",
"trademark-fill": "f21e",
"trademark-line": "f21f",
"advertisement-fill": "ea16",
"advertisement-line": "ea17",
"copyright-line": "ebe2",
"copyright-fill": "ebe1",
"creative-commons-nd-line": "ebf8",
"creative-commons-nd-fill": "ebf7",
"id-card-line": "f4e9",
"id-card-fill": "f4e8",
"info-card-line": "f57f",
"info-card-fill": "f57e",
"pass-pending-fill": "f388",
"pass-pending-line": "f389",
"pass-expired-fill": "f386",
"pass-expired-line": "f387",
"pass-valid-fill": "f38a",
"pass-valid-line": "f38b",
"megaphone-fill": "f384",
"megaphone-line": "f385",
"creative-commons-by-fill": "ebf1",
"creative-commons-by-line": "ebf2",
"creative-commons-fill": "ebf3",
"creative-commons-line": "ebf4",
"creative-commons-nc-line": "ebf6",
"creative-commons-nc-fill": "ebf5",
"copyleft-fill": "ebdf",
"copyleft-line": "ebe0",
"message-2-line": "ef44",
"message-2-fill": "ef43",
"chat-check-line": "eb55",
"chat-check-fill": "eb54",
"chat-unread-fill": "f529",
"chat-unread-line": "f52a",
"chat-new-line": "eb63",
"chat-new-fill": "eb62",
"chat-delete-fill": "eb56",
"chat-delete-line": "eb57",
"message-fill": "ef47",
"message-line": "ef48",
"chat-4-line": "eb53",
"chat-4-fill": "eb52",
"chat-settings-fill": "eb6c",
"chat-settings-line": "eb6d",
"chat-download-fill": "eb58",
"chat-download-line": "eb59",
"chat-upload-line": "eb75",
"chat-upload-fill": "eb74",
"chat-forward-fill": "eb5c",
"chat-forward-line": "eb5d",
"chat-heart-line": "eb5f",
"chat-heart-fill": "eb5e",
"chat-off-line": "eb65",
"chat-off-fill": "eb64",
"feedback-line": "ecc1",
"feedback-fill": "ecc0",
"question-answer-line": "f043",
"question-answer-fill": "f042",
"questionnaire-line": "f048",
"questionnaire-fill": "f047",
"speak-fill": "f36e",
"speak-line": "f36f",
"chat-thread-line": "f473",
"chat-thread-fill": "f472",
"chat-history-fill": "eb60",
"chat-history-line": "eb61",
"chat-private-line": "eb69",
"chat-private-fill": "eb68",
"emoji-sticker-line": "f37f",
"emoji-sticker-fill": "f37e",
"edit-line": "ec86",
"edit-fill": "ec85",
"markup-line": "ef20",
"markup-fill": "ef1f",
"edit-box-fill": "ec81",
"edit-box-line": "ec82",
"computer-line": "ebca",
"computer-fill": "ebc9",
"tv-line": "f237",
"tv-fill": "f236",
"smartphone-line": "f15a",
"smartphone-fill": "f159",
"device-fill": "ec2d",
"device-line": "ec2e",
"phone-line": "efec",
"phone-fill": "efe9",
"instance-fill": "f382",
"instance-line": "f383",
"database-2-line": "ec16",
"database-2-fill": "ec15",
"keyboard-box-fill": "ee72",
"keyboard-box-line": "ee73",
"shut-down-line": "f126",
"shut-down-fill": "f125",
"fingerprint-line": "ed31",
"fingerprint-fill": "ed30",
"barcode-box-line": "eaa0",
"barcode-box-fill": "ea9f",
"qr-code-line": "f03d",
"qr-code-fill": "f03c",
"qr-scan-fill": "f040",
"qr-scan-line": "f041",
"draft-line": "ec5c",
"draft-fill": "ec5b",
"file-paper-line": "ecfb",
"file-paper-fill": "ecfa",
"file-line": "eceb",
"file-fill": "ece0",
"sticky-note-fill": "f19a",
"sticky-note-line": "f19b",
"file-edit-line": "ecdb",
"file-edit-fill": "ecda",
"file-copy-fill": "ecd4",
"file-copy-line": "ecd5",
"bill-fill": "eac1",
"bill-line": "eac2",
"article-fill": "ea7d",
"article-line": "ea7e",
"survey-fill": "f1c6",
"survey-line": "f1c7",
"clipboard-line": "eb91",
"clipboard-fill": "eb90",
"news-fill": "f417",
"news-line": "f418",
"file-zip-fill": "ed1e",
"file-zip-line": "ed1f",
"todo-fill": "f216",
"todo-line": "f217",
"book-marked-line": "ead9",
"book-marked-fill": "ead8",
"task-fill": "f1e7",
"task-line": "f1e8",
"double-quotes-l": "ec51",
"double-quotes-r": "ec52",
"single-quotes-l": "f13b",
"single-quotes-r": "f13c",
"list-check": "eeba",
"list-ordered": "eebb",
"list-radio": "f39b",
"sort-asc": "f15f",
"sort-desc": "f160",
"send-backward": "f0d6",
"bring-forward": "eaf6",
"wallet-line": "f2ae",
"wallet-fill": "f2ad",
"bank-card-line": "ea92",
"bank-card-fill": "ea91",
"refund-line": "f068",
"refund-fill": "f067",
"safe-fill": "f0aa",
"safe-line": "f0ab",
"price-tag-line": "f025",
"price-tag-fill": "f024",
"ticket-line": "f20d",
"ticket-fill": "f20c",
"coupon-line": "ebee",
"coupon-fill": "ebed",
"shopping-bag-line": "f118",
"shopping-bag-fill": "f117",
"shopping-cart-line": "f120",
"shopping-cart-fill": "f11f",
"vip-line": "f292",
"vip-fill": "f291",
"vip-crown-2-line": "f28c",
"vip-crown-2-fill": "f28b",
"vip-diamond-fill": "f28f",
"vip-diamond-line": "f290",
"exchange-fill": "ecaa",
"exchange-line": "ecad",
"trophy-fill": "f22e",
"trophy-line": "f22f",
"swap-line": "f1cb",
"swap-fill": "f1ca",
"exchange-cny-line": "eca7",
"exchange-dollar-line": "eca9",
"exchange-funds-line": "ecac",
"copper-coin-line": "ebdc",
"copper-coin-fill": "ebdb",
"money-cny-box-line": "ef5f",
"money-cny-box-fill": "ef5e",
"money-cny-circle-line": "ef61",
"money-cny-circle-fill": "ef60",
"money-dollar-circle-line": "ef65",
"money-dollar-circle-fill": "ef64",
"increase-decrease-fill": "ee52",
"increase-decrease-line": "ee53",
"red-packet-fill": "f05f",
"red-packet-line": "f060",
"auction-fill": "ea87",
"auction-line": "ea88",
"gift-line": "edbb",
"gift-fill": "edba",
"24-hours-line": "ea02",
"nft-line": "f347",
"nft-fill": "f346",
"heart-fill": "ee0e",
"heart-line": "ee0f",
"heart-add-line": "ee0d",
"heart-add-fill": "ee0c",
"rest-time-fill": "f07d",
"rest-time-line": "f07e",
"apple-line": "ea40",
"apple-fill": "ea3f",
"alipay-fill": "ea2b",
"alipay-line": "ea2c",
"wechat-fill": "f2b5",
"wechat-line": "f2b6",
"wechat-pay-line": "f2b8",
"wechat-pay-fill": "f2b7",
"mini-program-fill": "ef5a",
"mini-program-line": "ef5b",
"android-line": "ea36",
"android-fill": "ea35",
"map-pin-line": "ef14",
"map-pin-fill": "ef13",
"map-pin-time-fill": "ef17",
"map-pin-time-line": "ef18",
"pushpin-fill": "f038",
"pushpin-line": "f039",
"unpin-line": "f377",
"unpin-fill": "f376",
"compass-fill": "ebc3",
"compass-line": "ebc4",
"earth-line": "ec7a",
"earth-fill": "ec79",
"parking-box-fill": "efcd",
"parking-box-line": "efce",
"navigation-fill": "ef88",
"navigation-line": "ef89",
"image-line": "ee4b",
"image-fill": "ee4a",
"multi-image-line": "f5ee",
"multi-image-fill": "f5ed",
"video-on-line": "f51e",
"video-on-fill": "f51d",
"clapperboard-line": "eb8f",
"clapperboard-fill": "eb8e",
"film-fill": "ed20",
"film-line": "ed21",
"movie-fill": "ef80",
"movie-line": "ef81",
"live-line": "eec0",
"live-fill": "eebf",
"vidicon-line": "f288",
"vidicon-fill": "f287",
"video-off-line": "f51c",
"video-off-fill": "f51b",
"camera-fill": "eb2e",
"camera-line": "eb31",
"camera-off-fill": "eb32",
"camera-off-line": "eb33",
"camera-lens-fill": "eb2f",
"camera-lens-line": "eb30",
"mv-line": "ef87",
"mv-fill": "ef86",
"music-2-fill": "ef82",
"music-2-line": "ef83",
"headphone-fill": "ee04",
"headphone-line": "ee05",
"mic-line": "ef50",
"mic-fill": "ef4f",
"mic-off-line": "ef52",
"mic-off-fill": "ef51",
"volume-down-fill": "f29b",
"volume-down-line": "f29c",
"volume-mute-line": "f29e",
"volume-mute-fill": "f29d",
"notification-4-line": "ef96",
"notification-4-fill": "ef95",
"notification-off-fill": "ef9b",
"notification-off-line": "ef9c",
"play-circle-line": "f009",
"play-circle-fill": "f008",
"pause-circle-line": "efd6",
"pause-circle-fill": "efd5",
"record-circle-line": "f05a",
"record-circle-fill": "f059",
"stop-circle-fill": "f19e",
"stop-circle-line": "f19f",
"fullscreen-line": "ed9c",
"fullscreen-exit-line": "ed9a",
"equalizer-2-line": "f405",
"equalizer-2-fill": "f404",
"apps-line": "ea44",
"apps-fill": "ea43",
"function-line": "ed9e",
"function-fill": "ed9d",
"dashboard-horizontal-line": "f4ce",
"dashboard-horizontal-fill": "f4cd",
"menu-line": "ef3e",
"menu-add-line": "ef3a",
"star-line": "f18b",
"star-fill": "f186",
"star-off-line": "f59b",
"star-off-fill": "f59a",
"more-line": "ef79",
"more-fill": "ef78",
"more-2-line": "ef77",
"more-2-fill": "ef76",
"settings-fill": "f0ed",
"settings-line": "f0ee",
"forbid-fill": "ed94",
"forbid-line": "ed95",
"prohibited-line": "f3a1",
"prohibited-fill": "f3a0",
"information-2-line": "f449",
"information-2-fill": "f448",
"error-warning-fill": "eca0",
"error-warning-line": "eca1",
"question-fill": "f044",
"question-line": "f045",
"checkbox-blank-circle-line": "eb7d",
"checkbox-blank-circle-fill": "eb7c",
"checkbox-circle-fill": "eb80",
"checkbox-circle-line": "eb81",
"checkbox-blank-line": "eb7f",
"checkbox-blank-fill": "eb7e",
"checkbox-line": "eb85",
"checkbox-fill": "eb82",
"add-circle-line": "ea11",
"add-circle-fill": "ea10",
"indeterminate-circle-fill": "ee56",
"indeterminate-circle-line": "ee57",
"close-circle-line": "eb97",
"close-circle-fill": "eb96",
"radio-button-line": "f050",
"radio-button-fill": "f04f",
"check-line": "eb7b",
"close-line": "eb99",
"add-line": "ea13",
"subtract-line": "f1af",
"divide-line": "ec40",
"equal-line": "f31f",
"upload-line": "f250",
"download-line": "ec5a",
"upload-cloud-2-line": "f24c",
"upload-cloud-2-fill": "f24b",
"download-cloud-2-line": "ec56",
"download-cloud-2-fill": "ec55",
"login-box-line": "eed4",
"login-box-fill": "eed3",
"shield-cross-line": "f102",
"shield-cross-fill": "f101",
"shield-check-fill": "f0ff",
"shield-check-line": "f100",
"delete-bin-fill": "ec29",
"delete-bin-line": "ec2a",
"lock-line": "eece",
"lock-fill": "eecd",
"lock-unlock-line": "eed2",
"lock-unlock-fill": "eed1",
"lock-password-line": "eed0",
"lock-password-fill": "eecf",
"eye-fill": "ecb4",
"eye-line": "ecb5",
"eye-off-line": "ecb7",
"eye-off-fill": "ecb6",
"search-line": "f0d1",
"search-fill": "f0d0",
"share-line": "f0fe",
"share-fill": "f0f7",
"share-box-line": "f0f4",
"share-box-fill": "f0f3",
"share-circle-line": "f0f6",
"share-circle-fill": "f0f5",
"time-fill": "f20e",
"time-line": "f20f",
"thumb-up-line": "f207",
"thumb-up-fill": "f206",
"notification-badge-fill": "ef97",
"notification-badge-line": "ef98",
"toggle-line": "f219",
"toggle-fill": "f218",
"filter-line": "ed27",
"filter-fill": "ed26",
"history-line": "ee17",
"loop-left-line": "f33d",
"loader-2-line": "eec2",
"loader-4-line": "eec6",
"reset-right-line": "f544",
"loader-fill": "eec9",
"user-3-line": "f256",
"user-3-fill": "f255",
"sun-fill": "f1bc",
"sun-line": "f1bf",
"moon-fill": "ef72",
"moon-line": "ef75",
"shining-line": "f35e",
"shining-fill": "f35d",
"fire-fill": "ed32",
"fire-line": "ed33",
"sparkling-line": "f36d",
"sparkling-fill": "f36c",
"box-1-line": "f2f1",
"box-1-fill": "f2f0",
"account-box-line": "ea07",
"account-box-fill": "ea06",
"account-circle-fill": "ea08",
"account-circle-line": "ea09",
"account-pin-box-fill": "ea0a",
"account-pin-box-line": "ea0b",
"skip-up-fill": "f366",
"arrow-left-right-fill": "ea61",
"arrow-up-down-fill": "ea73",
"arrow-up-double-fill": "f2ea",
"arrow-right-double-fill": "f2e4",
"expand-left-fill": "f320",
"expand-right-fill": "f324",
"expand-up-down-fill": "f326",
"expand-left-right-fill": "f322",
"arrow-go-back-fill": "ea57",
"arrow-go-forward-fill": "ea59",
"contract-left-line": "f2fd",
"contract-right-line": "f301",
"contract-right-fill": "f300",
"contract-left-fill": "f2fc",
"drag-move-line": "ec62",
"drag-move-fill": "ec61",
"home-line": "ee2b",
"home-fill": "ee26",
"mail-open-line": "eefa",
"mail-open-fill": "eef9",
"attachment-line": "ea86",
"attachment-fill": "ea85",
"bar-chart-fill": "ea99",
"bar-chart-horizontal-fill": "ea9c",
"bar-chart-2-fill": "ea95",
"bar-chart-grouped-fill": "ea9a",
"bubble-chart-fill": "eb02",
"pie-chart-fill": "eff9",
"calendar-schedule-line": "f3f3",
"calendar-schedule-fill": "f3f2",
"calendar-todo-line": "eb29",
"calendar-todo-fill": "eb28",
"calendar-event-fill": "eb24",
"calendar-event-line": "eb25",
"calendar-close-fill": "f38d",
"calendar-check-fill": "eb22",
"calendar-check-line": "eb23",
"calendar-close-line": "f38e",
"message-3-line": "ef46",
"message-3-fill": "ef45",
"chat-3-fill": "eb50",
"chat-3-line": "eb51",
"chat-1-fill": "eb4c",
"chat-1-line": "eb4d",
"chat-2-fill": "eb4e",
"chat-2-line": "eb4f",
"crop-line": "ec02",
"crop-fill": "ec01",
"palette-line": "efc5",
"palette-fill": "efc4",
"anticlockwise-line": "ea3c",
"anticlockwise-fill": "ea3b",
"clockwise-line": "eb95",
"clockwise-fill": "eb94",
"code-s-slash-fill": "ebac",
"code-s-slash-line": "ebad",
"puzzle-fill": "f451",
"puzzle-line": "f452",
"server-fill": "f0df",
"server-line": "f0e0",
"qr-scan-2-fill": "f03e",
"qr-scan-2-line": "f03f",
"scan-line": "f0bd",
"scan-fill": "f0bc",
"phone-find-fill": "efea",
"phone-find-line": "efeb",
"barcode-line": "eaa2",
"barcode-fill": "eaa1",
"file-list-fill": "ecf0",
"file-list-line": "ecf1",
"file-text-line": "ed0f",
"file-text-fill": "ed0e",
"book-fill": "ead6",
"book-line": "ead7",
"text": "f201",
"font-family": "f390",
"link": "eeb2",
"translate": "f227",
"copper-diamond-fill": "ebdd",
"copper-diamond-line": "ebde",
"dislike-fill": "ec3b",
"dislike-line": "ec3c",
"heart-3-fill": "ee0a",
"heart-3-line": "ee0b",
"hearts-fill": "ee12",
"hearts-line": "ee13",
"map-line": "ef08",
"map-fill": "ef07",
"image-circle-fill": "f412",
"image-circle-line": "f413",
"image-edit-fill": "ee48",
"image-edit-line": "ee49",
"image-add-line": "ee47",
"image-add-fill": "ee46",
"landscape-line": "ee7d",
"landscape-fill": "ee7c",
"check-double-line": "eb79",
"check-double-fill": "eb78",
"close-fill": "eb98",
"add-fill": "ea12",
"subtract-fill": "f1ae",
"divide-fill": "ec3f",
"equal-fill": "f31e",
"logout-circle-line": "eedc",
"logout-circle-fill": "eedb",
"shield-fill": "f103",
"shield-line": "f108",
"timer-line": "f215",
"timer-fill": "f212",
"delete-back-2-line": "ec1a",
"delete-back-2-fill": "ec19",
"volume-vibrate-line": "f2a4",
"volume-vibrate-fill": "f2a3",
"volume-off-vibrate-line": "f2a0",
"volume-off-vibrate-fill": "f29f",
"truck-line": "f231",
"truck-fill": "f230",
"flight-takeoff-line": "ed43",
"flight-takeoff-fill": "ed42",
"road-map-line": "f08e",
"road-map-fill": "f08d",
"pushpin-2-line": "f037",
"pushpin-2-fill": "f036",
"map-pin-2-line": "ef0a",
"map-pin-2-fill": "ef09",
"compass-discover-line": "ebc2",
"compass-discover-fill": "ebc1",
"signpost-fill": "f48d",
"signpost-line": "f48e",
"qq-line": "f03b",
"qq-fill": "f03a",
"tiktok-line": "f373",
"tiktok-fill": "f372",
"user-smile-line": "f274",
"user-smile-fill": "f273",
"user-line": "f264",
"user-fill": "f25f",
"user-add-fill": "f25d",
"user-add-line": "f25e",
"user-minus-line": "f54c",
"user-minus-fill": "f54b",
"user-follow-fill": "f260",
"user-follow-line": "f261",
"user-unfollow-line": "f278",
"user-unfollow-fill": "f277",
"user-shared-fill": "f271",
"user-shared-line": "f272",
"user-received-fill": "f269",
"user-received-line": "f26a",
"user-search-line": "f26c",
"user-search-fill": "f26b",
"user-location-line": "f266",
"user-location-fill": "f265",
"user-star-line": "f276",
"user-star-fill": "f275",
"user-settings-fill": "f26d",
"user-settings-line": "f26e",
"user-heart-line": "f263",
"user-heart-fill": "f262",
"user-forbid-line": "f3be",
"user-forbid-fill": "f3bd",
"group-fill": "ede2",
"group-line": "ede3",
"user-2-fill": "f253",
"user-2-line": "f254",
"shield-user-line": "f10c",
"shield-user-fill": "f10b",
"circle-line": "f3c2",
"circle-fill": "f3c1",
"sketching": "f35f",
"align-bottom": "ea24",
"restart-line": "f080",
"restart-fill": "f07f",
"refresh-line": "f064",
"refresh-fill": "f063",
"reset-left-line": "f542",
"reset-left-fill": "f541",
"skip-down-line": "f361",
"skip-down-fill": "f360",
"skip-right-line": "f365",
"skip-right-fill": "f364",
"skip-left-fill": "f362",
"skip-left-line": "f363",
"text-snippet": "f46e",
"input-method-line": "ee60",
"input-method-fill": "ee5f",
"font-size": "ed8d",
"font-size-2": "ed8c",
"font-color": "ed8b",
"node-tree": "ef90",
"price-tag-3-line": "f023",
"price-tag-3-fill": "f022",
"input-field": "f47a",
"timeline-view": "f46f",
"progress-2-line": "f47e",
"progress-2-fill": "f47d",
"t-box-line": "f1d3",
"t-box-fill": "f1d2",
"edit-2-fill": "ec7f",
"edit-2-line": "ec80",
"layout-2-line": "ee7f",
"layout-2-fill": "ee7e",
"layout-column-fill": "ee8c",
"layout-column-line": "ee8d",
"mouse-line": "ef7d",
"mouse-fill": "ef7c",
"file-upload-line": "ed15",
"file-upload-fill": "ed14",
"page-separator": "efbd",
"carousel-view": "f42c",
"list-view": "f44c",
"text-block": "f46d",
"percent-line": "efe6",
"percent-fill": "efe5",
"upload-fill": "f24f",
"t-shirt-line": "f1d9",
"t-shirt-fill": "f1d8",
"number-1": "efa0",
"check-fill": "eb7a",
"checkbox-multiple-line": "eb89",
"checkbox-multiple-fill": "eb88",
"collapse-vertical-line": "f52e",
"align-top": "ea29",
"window-2-line": "f2c4",
"window-2-fill": "f2c3",
"seo-line": "f3a4",
"seo-fill": "f3a3",
"shadow-line": "f45a",
"shadow-fill": "f459",
"puzzle-2-line": "f450",
"puzzle-2-fill": "f44f",
"markdown-line": "ef1e",
"markdown-fill": "ef1d",
"stacked-view": "f464",
"dropdown-list": "f3c3",
"timer-2-line": "f211",
"timer-2-fill": "f210",
"parent-line": "efca",
"parent-fill": "efc9",
"function-add-line": "f4df",
"function-add-fill": "f4de",
"arrow-up-box-line": "f562",
"arrow-up-box-fill": "f561",
"layout-bottom-fill": "ee8a",
"layout-right-fill": "ee9a",
"layout-right-line": "ee9b",
"layout-top-fill": "eea0",
"layout-top-line": "eea1",
"layout-left-line": "ee94",
"layout-left-fill": "ee93",
"layout-top-2-line": "ee9f",
"layout-top-2-fill": "ee9e",
"layout-right-2-line": "ee99",
"layout-right-2-fill": "ee98",
"layout-bottom-2-line": "ee89",
"layout-bottom-2-fill": "ee88",
"layout-left-2-line": "ee92",
"layout-left-2-fill": "ee91",
"layout-row-fill": "ee9c",
"layout-row-line": "ee9d",
"table-fill": "f1dd",
"table-line": "f1de",
"layout-bottom-line": "ee8b",
"picture-in-picture-line": "eff4",
"picture-in-picture-fill": "eff3",
"arrow-down-double-line": "f2e1",
"arrow-down-double-fill": "f2e0",
"arrow-left-double-fill": "f2e2",
"arrow-left-double-line": "f2e3"
};

37
cool-unix/index.html Normal file
View File

@@ -0,0 +1,37 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script>
var coverSupport =
"CSS" in window &&
typeof CSS.supports === "function" &&
(CSS.supports("top: env(a)") || CSS.supports("top: constant(a)"));
document.write(
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
(coverSupport ? ", viewport-fit=cover" : "") +
'" />'
);
</script>
<title></title>
<!--preload-links-->
<!--app-context-->
<style>
body,
html {
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
color: transparent;
}
}
</style>
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/main"></script>
</body>
</html>

2582
cool-unix/locale/en.json Normal file

File diff suppressed because it is too large Load Diff

2502
cool-unix/locale/es.json Normal file

File diff suppressed because it is too large Load Diff

2502
cool-unix/locale/fr.json Normal file

File diff suppressed because it is too large Load Diff

149
cool-unix/locale/index.ts Normal file
View File

@@ -0,0 +1,149 @@
import { isNull, forInObject, isEmpty, storage, router } from "@/cool";
import { ref } from "vue";
import zhcn from "./zh-cn.json";
import zhtw from "./zh-tw.json";
import en from "./en.json";
import es from "./es.json";
import ja from "./ja.json";
import ko from "./ko.json";
import fr from "./fr.json";
import { config } from "@/config";
// 解析语言包
function parse(val: string[][]) {
const isCustom = val.length == 1 && val[0].length == 1;
if (!isCustom) {
return val;
}
return val[0][0].split("<__&__>").map((e) => e.split("<__=__>"));
}
/**
* 语言包映射对象,包含所有已支持的语言。
* 如需新增语言,只需新建对应的 xx.json 文件并在此处引入即可。
*/
const messages = {
"zh-cn": parse(zhcn),
"zh-tw": parse(zhtw),
en: parse(en),
es: parse(es),
ja: parse(ja),
ko: parse(ko),
fr: parse(fr)
};
// 当前语言,默认中文
export const locale = ref<string>("");
// 设置当前语言
export const setLocale = (value: string) => {
locale.value = value;
// #ifdef APP
// APP 环境下,存储语言到本地
storage.set("locale", value, 0);
// #endif
// #ifndef APP
// 其他环境下,直接设置全局语言
uni.setLocale(value);
// #endif
};
// 获取当前语言
export const getLocale = (): string => {
let value: string;
// #ifdef APP
// APP 环境下,优先从本地存储获取
const _locale = storage.get("locale") as string | null;
if (_locale != null && !isEmpty(_locale)) {
value = _locale;
} else {
// @ts-ignore
value = uni.getDeviceInfo().osLanguage as string;
}
// #endif
// #ifndef APP
// 其他环境下,直接获取全局语言
value = uni.getLocale();
// #endif
if (isNull(value) || isEmpty(value)) {
value = config.locale;
}
return value;
};
// 追加数据
export const appendLocale = (name: string, data: string[][]) => {
if (messages[name] != null) {
(messages[name] as string[][]).unshift(...parse(data));
}
};
// 不带参数的翻译方法
export const t = (name: string) => {
let data = messages[locale.value] as string[][] | null;
if (data == null) {
return name;
}
let text = data.find((e) => e[0] == name)?.[1];
if (text == null || text == "") {
text = name;
}
return text;
};
// 带参数的翻译方法
export const $t = (name: string, data: any) => {
let text = t(name);
// 替换参数
if (!isNull(data)) {
forInObject(data, (value, key) => {
if (typeof value === "number") {
value = value.toString();
}
text = text.replaceAll(`{${key}}`, value as string);
});
}
return text;
};
// 初始化语言设置
export const initLocale = () => {
locale.value = getLocale();
// #ifndef APP
// 监听语言切换事件,自动更新 locale
uni.onLocaleChange((res) => {
setLocale(res.locale!);
});
// #endif
};
// 更新标题
export function updateTitle() {
const style = router.route()?.style;
if (style != null) {
if (style.navigationBarTitleText != null) {
uni.setNavigationBarTitle({
title: t(style.navigationBarTitleText as string)
});
}
}
}

2502
cool-unix/locale/ja.json Normal file

File diff suppressed because it is too large Load Diff

2502
cool-unix/locale/ko.json Normal file

File diff suppressed because it is too large Load Diff

2582
cool-unix/locale/zh-cn.json Normal file

File diff suppressed because it is too large Load Diff

2502
cool-unix/locale/zh-tw.json Normal file

File diff suppressed because it is too large Load Diff

14
cool-unix/main.ts Normal file
View File

@@ -0,0 +1,14 @@
import { createSSRApp } from "vue";
import { cool } from "./cool";
import App from "./App.uvue";
import "./router";
export function createApp() {
const app = createSSRApp(App);
cool(app);
return {
app
};
}

94
cool-unix/manifest.json Normal file
View File

@@ -0,0 +1,94 @@
{
"name": "cool-unix",
"appid": "__UNI__651711F",
"description": "完全开源、永久免费、上手容易、效率极高的开发脚手架",
"versionName": "1.0.0",
"versionCode": "100",
"uni-app-x": {},
"quickapp": {},
"mp-weixin": {
"darkmode": true,
"appid": "wxdebc4de0b5584ca4",
"setting": {
"urlCheck": false,
"es6": true
},
"usingComponents": true
},
"mp-alipay": {
"usingComponents": true
},
"mp-baidu": {
"usingComponents": true
},
"mp-toutiao": {
"usingComponents": true
},
"uniStatistics": {
"enable": false
},
"vueVersion": "3",
"app": {
"distribute": {
"icons": {
"android": {
"hdpi": "",
"xhdpi": "",
"xxhdpi": "",
"xxxhdpi": ""
}
}
}
},
"web": {
"darkmode": true,
"router": {
"mode": "",
"base": "./"
},
"title": "cool-unix"
},
"app-harmony": {
"distribute": {
"bundleName": "com.cool.unix",
"signingConfigs": {
"default": {
"certpath": "/Users/icssoa/Library/Application Support/HBuilder X/extensions/launcher/agc-certs/1749115146522.cer",
"keyAlias": "debugKey",
"keyPassword": "0000001B0CD2170B509D76F6435F878B7ED2FE2E3EA6E7454E26523487B093238D4F7C8B7033D30DE80163",
"profile": "/Users/icssoa/Library/Application Support/HBuilder X/extensions/launcher/agc-certs/1749115146522.p7b",
"signAlg": "SHA256withECDSA",
"storeFile": "/Users/icssoa/Library/Application Support/HBuilder X/extensions/launcher/agc-certs/1749115146522.p12",
"storePassword": "0000001B0CD2170B509D76F6435F878B7ED2FE2E3EA6E7454E26523487B093238D4F7C8B7033D30DE80163"
}
},
"icons": {
"foreground": "static/logo2.png",
"background": "static/logo2.png"
}
}
},
"app-android": {
"distribute": {
"modules": {},
"icons": {
"hdpi": "docs/logo.png",
"xhdpi": "docs/logo.png",
"xxhdpi": "docs/logo.png",
"xxxhdpi": "docs/logo.png"
},
"splashScreens": {
"default": {}
}
}
},
"app-ios": {
"distribute": {
"modules": {},
"icons": {
"appstore": "docs/logo.png"
},
"splashScreens": {}
}
}
}

30
cool-unix/package.json Normal file
View File

@@ -0,0 +1,30 @@
{
"name": "cool-unix",
"version": "8.0.30",
"license": "MIT",
"scripts": {
"build-ui": "node ./uni_modules/cool-ui/scripts/generate-types.js",
"build-icon": "node ./.cool/scripts/icon.js"
},
"dependencies": {
"hammer-touchemulator": "^0.0.2",
"vue": "^3.5.13",
"weixin-js-sdk": "^1.6.5"
},
"devDependencies": {
"@babel/parser": "^7.27.5",
"@babel/types": "^7.27.6",
"@cool-vue/ai": "^1.1.7",
"@cool-vue/vite-plugin": "^8.2.18",
"@dcloudio/types": "^3.4.16",
"@types/node": "^24.0.15",
"@vue/compiler-sfc": "^3.5.16",
"@vue/tsconfig": "^0.7.0",
"adm-zip": "^0.5.16",
"autoprefixer": "^10.4.21",
"postcss": "^8.5.3",
"prettier": "^3.5.3",
"tailwindcss": "3.4.17",
"vite": "^6.3.5"
}
}

692
cool-unix/pages.json Normal file
View File

@@ -0,0 +1,692 @@
{
"pages": [
{
"path": "pages/index/home",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/index/my",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/index/template",
"style": {
"navigationBarTitleText": "模板"
}
}
],
"subPackages": [
{
"root": "pages/set",
"pages": [
{
"path": "index",
"style": {
"navigationBarTitleText": "设置"
},
"meta": {
"isAuth": true
}
},
{
"path": "general",
"style": {
"navigationBarTitleText": "通用设置"
},
"meta": {
"isAuth": true
}
},
{
"path": "notice",
"style": {
"navigationBarTitleText": "通知设置"
},
"meta": {
"isAuth": true
}
},
{
"path": "about",
"style": {
"navigationBarTitleText": ""
},
"meta": {
"isAuth": true
}
},
{
"path": "cs",
"style": {
"navigationBarTitleText": "联系客服"
},
"meta": {
"isAuth": true
}
}
]
},
{
"root": "pages/monthlyreport",
"pages": [
{
"path": "list",
"style": { "navigationBarTitleText": "我的月报" },
"meta": { "isAuth": true }
},
{
"path": "detail",
"style": { "navigationBarTitleText": "月报详情" },
"meta": { "isAuth": true }
},
{
"path": "submit",
"style": { "navigationBarTitleText": "提交月报" },
"meta": { "isAuth": true }
}
]
},
{
"root": "pages/advice",
"pages": [
{
"path": "index",
"style": { "navigationBarTitleText": "工作建议" },
"meta": { "isAuth": true }
}
]
},
{
"root": "pages/user",
"pages": [
{
"path": "edit",
"style": {
"navigationBarTitleText": "编辑资料"
},
"meta": {
"isAuth": true
}
},
{
"path": "edit-name",
"style": {
"navigationStyle": "custom"
},
"meta": {
"isAuth": true
}
},
{
"path": "edit-description",
"style": {
"navigationStyle": "custom"
},
"meta": {
"isAuth": true
}
},
{
"path": "login",
"style": {
"navigationStyle": "custom",
"disableScroll": true
}
}
]
},
{
"root": "pages/demo",
"pages": [
{
"path": "basic/text",
"style": {
"navigationBarTitleText": "Text 文本"
}
},
{
"path": "basic/button",
"style": {
"navigationBarTitleText": "Button 按钮"
}
},
{
"path": "basic/image",
"style": {
"navigationBarTitleText": "Image 图片"
}
},
{
"path": "basic/icon",
"style": {
"navigationBarTitleText": "Icon 图标"
}
},
{
"path": "basic/tag",
"style": {
"navigationBarTitleText": "Tag 标签"
}
},
{
"path": "form/form",
"style": {
"navigationBarTitleText": "Form 表单验证"
}
},
{
"path": "form/input",
"style": {
"navigationBarTitleText": "Input 输入框"
}
},
{
"path": "form/textarea",
"style": {
"navigationBarTitleText": "Textarea 文本域"
}
},
{
"path": "form/input-number",
"style": {
"navigationBarTitleText": "InputNumber 计数器"
}
},
{
"path": "form/input-otp",
"style": {
"navigationBarTitleText": "InputOtp 口令输入"
}
},
{
"path": "form/keyboard",
"style": {
"navigationBarTitleText": "Keyboard 键盘"
}
},
{
"path": "form/radio",
"style": {
"navigationBarTitleText": "Radio 单选框"
}
},
{
"path": "form/checkbox",
"style": {
"navigationBarTitleText": "Checkbox 多选框"
}
},
{
"path": "form/switch",
"style": {
"navigationBarTitleText": "Switch 开关"
}
},
{
"path": "form/rate",
"style": {
"navigationBarTitleText": "Rate 评分"
}
},
{
"path": "form/slider",
"style": {
"navigationBarTitleText": "Slider 滑块"
}
},
{
"path": "form/select",
"style": {
"navigationBarTitleText": "Select 选择器"
}
},
{
"path": "form/select-date",
"style": {
"navigationBarTitleText": "SelectDate 日期选择器"
}
},
{
"path": "form/select-time",
"style": {
"navigationBarTitleText": "SelectTime 时间选择器"
}
},
{
"path": "form/cascader",
"style": {
"navigationBarTitleText": "Cascader 级联选择器"
}
},
{
"path": "form/upload",
"style": {
"navigationBarTitleText": "Upload 文件上传"
}
},
{
"path": "form/calendar",
"style": {
"navigationBarTitleText": "Calendar 日历"
}
},
{
"path": "layout/flex",
"style": {
"navigationBarTitleText": "Flex 弹性布局"
}
},
{
"path": "layout/tabs",
"style": {
"navigationBarTitleText": "Tabs 标签页"
}
},
{
"path": "layout/collapse",
"style": {
"navigationBarTitleText": "Collapse 折叠面板"
}
},
{
"path": "layout/sticky",
"style": {
"navigationBarTitleText": "Sticky 吸顶"
}
},
{
"path": "layout/topbar",
"style": {
"navigationBarTitleText": "TopBar 导航栏"
}
},
{
"path": "layout/float-view",
"style": {
"navigationBarTitleText": "FloatView 悬浮视图"
}
},
{
"path": "layout/footer",
"style": {
"navigationBarTitleText": "Footer 底部视图"
}
},
{
"path": "data/list",
"style": {
"navigationBarTitleText": "List 列表"
}
},
{
"path": "data/list-view",
"style": {
"navigationBarTitleText": "ListView 列表视图"
}
},
{
"path": "data/list-view-refresh",
"style": {
"navigationBarTitleText": "ListViewRefresh 列表刷新"
}
},
{
"path": "data/waterfall",
"style": {
"navigationBarTitleText": "Waterfall 瀑布流"
}
},
{
"path": "data/banner",
"style": {
"navigationBarTitleText": "Banner 轮播图"
}
},
{
"path": "data/marquee",
"style": {
"navigationBarTitleText": "Marquee 跑马灯"
}
},
{
"path": "data/pagination",
"style": {
"navigationBarTitleText": "Pagination 分页"
}
},
{
"path": "data/timeline",
"style": {
"navigationBarTitleText": "Timeline 时间轴"
}
},
{
"path": "data/avatar",
"style": {
"navigationBarTitleText": "Avatar 头像"
}
},
{
"path": "data/read-more",
"style": {
"navigationBarTitleText": "ReadMore 查看更多"
}
},
{
"path": "data/draggable",
"style": {
"navigationBarTitleText": "Draggable 拖拽"
}
},
{
"path": "data/filter-bar",
"style": {
"navigationBarTitleText": "FilterBar 筛选栏"
}
},
{
"path": "data/tree",
"style": {
"navigationBarTitleText": "Tree 树形结构"
}
},
{
"path": "status/badge",
"style": {
"navigationBarTitleText": "Badge 角标"
}
},
{
"path": "status/noticebar",
"style": {
"navigationBarTitleText": "NoticeBar 通知栏"
}
},
{
"path": "status/countdown",
"style": {
"navigationBarTitleText": "Countdown 倒计时"
}
},
{
"path": "status/progress",
"style": {
"navigationBarTitleText": "Progress 进度条"
}
},
{
"path": "status/progress-circle",
"style": {
"navigationBarTitleText": "ProgressCircle 圆形进度条"
}
},
{
"path": "status/skeleton",
"style": {
"navigationBarTitleText": "Skeleton 骨架图"
}
},
{
"path": "status/loadmore",
"style": {
"navigationBarTitleText": "LoadMore 加载更多"
}
},
{
"path": "status/rolling-number",
"style": {
"navigationBarTitleText": "RollingNumber 数字滚动"
}
},
{
"path": "feedback/action-sheet",
"style": {
"navigationBarTitleText": "ActionSheet 操作菜单"
}
},
{
"path": "feedback/popup",
"style": {
"navigationBarTitleText": "Popup 弹窗"
}
},
{
"path": "feedback/confirm",
"style": {
"navigationBarTitleText": "Confirm 确认框"
}
},
{
"path": "feedback/toast",
"style": {
"navigationBarTitleText": "Toast 提示框"
}
},
{
"path": "other/qrcode",
"style": {
"navigationBarTitleText": "QRCode 二维码"
}
},
{
"path": "other/sign",
"style": {
"navigationBarTitleText": "Sign 签名"
}
},
{
"path": "other/watermark",
"style": {
"navigationBarTitleText": "Watermark 水印"
}
},
{
"path": "other/day-uts",
"style": {
"navigationBarTitleText": "DayUts 日期"
}
},
{
"path": "other/vibrate",
"style": {
"navigationBarTitleText": "Vibrate 震动"
}
},
{
"path": "other/cropper",
"style": {
"navigationBarTitleText": "Cropper 图片裁剪"
}
},
{
"path": "other/canvas",
"style": {
"navigationBarTitleText": "Canvas 画布"
}
},
{
"path": "other/svg",
"style": {
"navigationBarTitleText": "SVG 图标"
}
},
{
"path": "other/slide-verify",
"style": {
"navigationBarTitleText": "SlideVerify 滑动验证"
}
},
{
"path": "other/animation",
"style": {
"navigationBarTitleText": "Animation 动画"
}
},
{
"path": "other/router/index",
"style": {
"navigationBarTitleText": "Router 路由"
}
},
{
"path": "other/router/query"
},
{
"path": "other/share",
"style": {
"navigationBarTitleText": "Share 分享"
}
}
]
},
{
"root": "pages/template",
"pages": [
{
"path": "post/detail",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "shop/goods-category",
"style": {
"navigationBarTitleText": "商品分类"
}
},
{
"path": "shop/goods-detail/index",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "shop/shopping-cart",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "shop/address",
"style": {
"navigationBarTitleText": "收货地址",
"enablePullDownRefresh": true
},
"meta": {
"isAuth": true
}
},
{
"path": "shop/address-edit",
"style": {
"navigationBarTitleText": "编辑地址"
}
}
]
},
{
"root": "pages/dailyreport",
"pages": [
{
"path": "list",
"style": {
"navigationBarTitleText": "我的日报"
},
"meta": {
"isAuth": true
}
},
{
"path": "submit",
"style": {
"navigationBarTitleText": "提交日报"
},
"meta": {
"isAuth": true
}
},
{
"path": "detail",
"style": {
"navigationBarTitleText": "日报详情"
},
"meta": {
"isAuth": true
}
}
]
},
{
"root": "pages/weeklyreport",
"pages": [
{
"path": "list",
"style": {
"navigationBarTitleText": "我的周报"
},
"meta": {
"isAuth": true
}
},
{
"path": "submit",
"style": {
"navigationBarTitleText": "提交周报"
},
"meta": {
"isAuth": true
}
},
{
"path": "detail",
"style": {
"navigationBarTitleText": "周报详情"
},
"meta": {
"isAuth": true
}
}
]
}
],
"globalStyle": {
"navigationBarTitleText": "cool-unix",
"navigationBarTextStyle": "@navTextStyle",
"backgroundColorContent": "@bgContentColor",
"backgroundColor": "@bgColor",
"navigationBarBackgroundColor": "@navBgColor"
},
"tabBar": {
"custom": true,
"color": "@tabColor",
"selectedColor": "@tabSelectedColor",
"backgroundColor": "@tabBgColor",
"borderStyle": "@tabBorderStyle",
"height": "60px",
"list": [
{
"pagePath": "pages/index/home",
"iconPath": "/static/icon/tabbar/home.png",
"selectedIconPath": "/static/icon/tabbar/home2.png",
"text": "首页"
},
{
"pagePath": "pages/index/template",
"iconPath": "/static/icon/tabbar/template.png",
"selectedIconPath": "/static/icon/tabbar/template2.png",
"text": "模板"
},
{
"pagePath": "pages/index/my",
"iconPath": "/static/icon/tabbar/my.png",
"selectedIconPath": "/static/icon/tabbar/my2.png",
"text": "我的"
}
]
},
"uniIdRouter": {}
}

View File

@@ -0,0 +1,129 @@
<template>
<cl-page title="工作建议">
<view class="p-4">
<!-- 范围选择 -->
<!-- 行1范围 + 周/月 -->
<view class="flex items-center gap-2 mb-2">
<text class="label">范围:</text>
<view class="chip" :class="{ active: scope==='week' }" @tap="setScope('week')">周</view>
<view class="chip" :class="{ active: scope==='month' }" @tap="setScope('month')">月</view>
</view>
<!-- 行2日期输入 + 刷新(左侧成组,右侧按钮) -->
<view class="flex items-center justify-between mb-4">
<view class="flex items-center gap-2 flex-1">
<template v-if="scope === 'week'">
<text class="text-gray-500">周起始(周一):</text>
<view class="flex-1">
<cl-input v-model="weekStart" type="date" placeholder="选择本周周一" />
</view>
</template>
<template v-else>
<text class="text-gray-500">月份YYYY-MM</text>
<view class="flex-1">
<cl-input v-model="month" placeholder="例如 2025-11" />
</view>
</template>
</view>
<cl-button type="primary" size="small" style="margin-left: 5px;" @tap="loadAdvice">刷新</cl-button>
</view>
<!-- 建议内容 -->
<view v-if="loading" class="py-12 text-center text-gray-400">加载中...</view>
<view v-else>
<view v-if="adviceText" class="p-4 bg-white rounded-2xl whitespace-pre-wrap">{{ adviceText }}</view>
<view v-else class="p-8 text-center text-gray-400">{{ emptyText }}</view>
</view>
</view>
</cl-page>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { request, useStore } from '@/cool';
import { useUi } from '@/uni_modules/cool-ui';
const ui = useUi();
const { user } = useStore();
const scope = ref<'week' | 'month'>('week');
const weekStart = ref(''); // yyyy-MM-dd周一
const month = ref(''); // yyyy-MM
const adviceText = ref('');
const emptyText = ref('暂无建议,请调整时间范围后重试');
const loading = ref(false);
onMounted(async () => {
if (user.token) { try { await user.get(); } catch {} }
// 默认周:设置为当前周周一;默认月:当前月
const today = new Date();
const monday = new Date(today);
const day = monday.getDay();
const diff = (day === 0 ? -6 : 1 - day); // 周一为 1周日为 0
monday.setDate(monday.getDate() + diff);
weekStart.value = fmtYMD(monday);
month.value = `${monday.getFullYear()}-${String(monday.getMonth() + 1).padStart(2,'0')}`;
loadAdvice();
});
function setScope(s: 'week' | 'month') {
scope.value = s;
}
function fmtYMD(d: Date) {
const Y = d.getFullYear();
const M = String(d.getMonth() + 1).padStart(2, '0');
const D = String(d.getDate()).padStart(2, '0');
return `${Y}-${M}-${D}`;
}
async function loadAdvice() {
try {
loading.value = true;
adviceText.value = '';
if (!user.info.value?.id) {
return ui.showToast({ message: '请先登录', type: 'error' });
}
const params: any = { scope: scope.value, userId: user.info.value.id };
if (scope.value === 'week') {
if (!weekStart.value) return ui.showToast({ message: '请选择周一日期', type: 'error' });
params.startDate = weekStart.value;
} else {
const m = (month.value || '').trim();
if (!/^\d{4}-\d{2}$/.test(m)) return ui.showToast({ message: '请输入有效月份,如 2025-11', type: 'error' });
params.startDate = `${m}-01`;
}
const res = await request({ url: '/app/useradvice/advice', method: 'GET', params });
adviceText.value = res?.adviceText || '';
if (!adviceText.value) {
emptyText.value = scope.value === 'month'
? '未查询到该月份的月报/周报/日报内容,请核实后再尝试'
: '未查询到该周的可用内容,请核实后再尝试';
}
} catch (e: any) {
adviceText.value = '';
emptyText.value = e?.message || (scope.value === 'month'
? '未查询到该月份的月报/周报/日报内容,请核实后再尝试'
: '未查询到该周的可用内容,请核实后再尝试');
} finally {
loading.value = false;
}
}
</script>
<style scoped>
.label { color: #6b7280; /* gray-500 */ margin-right: 8px; }
.chip {
display: inline-flex;
align-items: center;
padding: 4px 12px;
border-radius: 9999px;
border: 1px solid #d1d5db; /* gray-300 */
color: #374151; /* gray-700 */
}
.chip.active {
background: #3b82f6; /* blue-500 */
border-color: #3b82f6;
color: #ffffff;
}
</style>

View File

@@ -0,0 +1,222 @@
<template>
<cl-page title="日报详情">
<view v-if="loading" class="flex justify-center items-center py-20">
<cl-loading />
</view>
<view v-else-if="report" class="p-4">
<!-- 头部信息 -->
<view class="mb-6">
<view class="flex justify-between items-center mb-2">
<text class="text-2xl font-bold">{{ formatDate(report.reportDate) }}</text>
<cl-tag :type="report.status === 1 ? 'success' : 'warning'">
{{ report.status === 1 ? '已提交' : '草稿' }}
</cl-tag>
</view>
<text class="text-sm text-gray-500">
{{ report.inputType === 1 ? "🎤 语音输入" : "⌨️ 文字输入" }} ·
{{ report.submitTime ? ('提交于 ' + formatDateTime(report.submitTime)) : ('创建于 ' + formatDateTime(report.createTime)) }}
</text>
</view>
<!-- 最终日报内容 -->
<view class="mb-6">
<text class="text-base font-bold mb-3 block">📝 日报内容</text>
<view class="p-4 bg-white rounded-lg shadow-sm">
<text class="text-sm text-gray-800">{{ report.userEditedContent || "暂无内容" }}</text>
</view>
</view>
<!-- 折叠面板查看原始内容和AI生成内容 -->
<cl-collapse v-model="activeNames">
<!-- 原始内容 -->
<cl-collapse-item v-if="report.originalText" name="original" title="📄 原始输入内容">
<view class="p-3 bg-gray-50 rounded">
<text class="text-sm text-gray-700">{{ report.originalText }}</text>
</view>
</cl-collapse-item>
<!-- AI格式化内容 -->
<cl-collapse-item v-if="report.aiFormattedContent" name="ai" title="🤖 AI生成内容">
<view class="p-3 bg-blue-50 rounded">
<text class="text-sm text-gray-800">{{ report.aiFormattedContent }}</text>
</view>
</cl-collapse-item>
</cl-collapse>
<!-- 操作按钮 -->
<view class="flex gap-3 mt-6">
<cl-button type="primary" size="large" :flex="1" @tap="editReport">
编辑日报
</cl-button>
<cl-button v-if="report.status === 0" type="success" size="large" :flex="1" @tap="submitReport" :loading="isSubmitting">
{{ isSubmitting ? "提交中..." : "提交日报" }}
</cl-button>
</view>
</view>
<view v-else class="flex flex-col items-center justify-center py-20">
<text class="text-6xl mb-4">📝</text>
<text class="text-gray-400 text-base">日报不存在</text>
<cl-button type="primary" size="small" class="mt-4" @tap="router.back()">
返回列表
</cl-button>
</view>
</cl-page>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { request, router, useStore } from "@/cool";
import { useUi } from "@/uni_modules/cool-ui";
import { onLoad } from "@dcloudio/uni-app";
const ui = useUi();
const { user } = useStore();
// 日报数据
const report = ref<any>(null);
const loading = ref(true);
const isSubmitting = ref(false);
// 折叠面板展开项
const activeNames = ref<string[]>([]);
// 用户ID从登录状态获取
const userId = ref(0);
// 路由参数onLoad 中获取)
const reportId = ref("");
const reportDateParam = ref("");
onLoad(async (options: any) => {
// 读取路由参数
reportId.value = (options?.id ?? "").toString();
reportDateParam.value = (options?.date ?? "").toString();
// 刷新用户信息
if (user.token) {
try { await user.get(); } catch (e) { console.error("【日报详情】获取用户信息失败:", e); }
}
// 获取当前登录用户ID
if (user.info.value && user.info.value.id) {
userId.value = user.info.value.id;
} else {
ui.showToast({ message: "请先登录", type: "error" });
setTimeout(() => { router.to("/pages/user/login"); }, 1000);
loading.value = false;
return;
}
if (reportId.value) {
await loadReportDetail();
} else if (reportDateParam.value) {
await loadReportDetailByDateFallback();
} else {
loading.value = false;
}
});
// 加载日报详情按ID
async function loadReportDetail() {
loading.value = true;
try {
const res = await request({
url: "/app/dailyreport/report/detail",
method: "GET",
params: { id: reportId.value, userId: userId.value }
});
if (res) { report.value = res; return; }
await loadReportDetailByDateFallback();
} catch (error: any) {
console.error("加载日报详情失败:", error);
await loadReportDetailByDateFallback();
} finally {
loading.value = false;
}
}
// 按日期回退查询
async function loadReportDetailByDateFallback() {
if (!reportDateParam.value) return;
loading.value = true;
try {
const res = await request({
url: "/app/dailyreport/report/myReports",
method: "GET",
params: {
userId: userId.value,
startDate: reportDateParam.value,
endDate: reportDateParam.value
}
});
if (Array.isArray(res) && res.length > 0) {
report.value = res[0];
}
} catch (error: any) {
console.error("按日期加载日报失败:", error);
} finally {
loading.value = false;
}
}
// 编辑日报
function editReport() {
const id = reportId.value || report.value?.id;
if (id) {
router.to(`/pages/dailyreport/submit?id=${id}`);
} else if (reportDateParam.value) {
router.to(`/pages/dailyreport/submit?date=${reportDateParam.value}`);
} else {
ui.showToast({ message: "无有效日报标识", type: "warn" });
}
}
// 提交日报(草稿转提交)
async function submitReport() {
if (!report.value) return;
isSubmitting.value = true;
try {
await request({
url: "/app/dailyreport/report/submit",
method: "POST",
data: {
userId: userId.value,
reportDate: report.value.reportDate,
originalText: report.value.originalText,
aiFormattedContent: report.value.aiFormattedContent,
userEditedContent: report.value.userEditedContent,
inputType: report.value.inputType
}
});
ui.showToast({ message: "日报提交成功", type: "success" });
setTimeout(() => { loadReportDetail(); }, 500);
} catch (error: any) {
console.error("提交日报失败:", error);
ui.showToast({ message: "提交失败: " + (error.message || "未知错误"), type: "error" });
} finally {
isSubmitting.value = false;
}
}
// 工具:日期与时间格式化
function formatDate(dateStr: string) {
if (!dateStr) return "";
const d = new Date(dateStr);
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
const dd = String(d.getDate()).padStart(2, "0");
return `${y}-${m}-${dd}`;
}
function formatDateTime(timeStr: string) {
if (!timeStr) return "";
const date = new Date(timeStr);
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, "0");
const d = String(date.getDate()).padStart(2, "0");
const hh = String(date.getHours()).padStart(2, "0");
const mm = String(date.getMinutes()).padStart(2, "0");
return `${y}-${m}-${d} ${hh}:${mm}`;
}
</script>

View File

@@ -0,0 +1,327 @@
<template>
<cl-page title="我的日报">
<!-- 筛选栏 -->
<view class="px-4 py-3 bg-white border-b">
<view class="flex gap-2">
<cl-button
:type="filterType === 'all' ? 'primary' : 'default'"
size="small"
@tap="changeFilter('all')"
>
全部
</cl-button>
<cl-button
:type="filterType === 'draft' ? 'primary' : 'default'"
size="small"
@tap="changeFilter('draft')"
>
草稿
</cl-button>
<cl-button
:type="filterType === 'submitted' ? 'primary' : 'default'"
size="small"
@tap="changeFilter('submitted')"
>
已提交
</cl-button>
</view>
</view>
<!-- 日报列表 -->
<scroll-view scroll-y class="flex-1">
<!-- 加载中 -->
<view v-if="loading && list.length === 0" class="flex items-center justify-center py-20">
<text class="text-gray-400">加载中...</text>
</view>
<!-- 列表内容 -->
<view v-else-if="list.length > 0">
<view
v-for="(item, index) in list"
:key="item.id || index"
class="mx-4 my-2 p-4 bg-white rounded-lg shadow-sm"
@tap="toDetail(item)"
>
<!-- 头部:日期和状态 -->
<view class="flex justify-between items-center mb-3">
<view class="flex items-center gap-2">
<text class="text-lg font-bold">{{ formatDate(item.reportDate) }}</text>
<text class="text-xs text-gray-500">
{{ formatWeekday(item.reportDate) }}
</text>
</view>
<view
:class="[
'px-2 py-1 rounded text-xs',
item.status === 1 ? 'bg-green-100 text-green-700' : 'bg-yellow-100 text-yellow-700'
]"
>
{{ item.status === 1 ? "已提交" : "草稿" }}
</view>
</view>
<!-- 内容摘要 -->
<view class="mb-3">
<text class="text-sm text-gray-700">
{{ getContentPreview(item.userEditedContent) }}
</text>
</view>
<!-- 底部信息 -->
<view class="flex justify-between items-center text-xs text-gray-500">
<view class="flex items-center gap-2">
<text>{{ item.inputType === 1 ? "🎤 语音输入" : "⌨️ 文字输入" }}</text>
</view>
<text v-if="item.submitTime">
提交于 {{ formatTime(item.submitTime) }}
</text>
<text v-else-if="item.createTime">
创建于 {{ formatTime(item.createTime) }}
</text>
</view>
</view>
<!-- 加载更多提示 -->
<view v-if="loading" class="flex items-center justify-center py-4">
<text class="text-gray-400 text-sm">加载中...</text>
</view>
<view v-else-if="finished" class="flex items-center justify-center py-4">
<text class="text-gray-400 text-sm">没有更多了</text>
</view>
</view>
<!-- 空状态 -->
<view v-else class="flex flex-col items-center justify-center py-20">
<text class="text-6xl mb-4">📝</text>
<text class="text-gray-400 text-base">暂无日报</text>
<view
class="mt-4 px-4 py-2 bg-blue-500 text-white rounded"
@tap="toSubmit"
>
<text>去提交日报</text>
</view>
</view>
</scroll-view>
<!-- 悬浮提交按钮 -->
<view class="fixed bottom-20 right-4">
<cl-button
type="primary"
size="large"
round
@tap="toSubmit"
>
<text class="text-2xl">✏️</text>
</cl-button>
</view>
</cl-page>
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { request, router, useStore } from "@/cool";
import { useUi } from "@/uni_modules/cool-ui";
const ui = useUi();
const { user } = useStore();
// 列表相关
const list = ref<any[]>([]);
const loading = ref(false);
const finished = ref(false);
const page = ref(1);
const pageSize = ref(20);
// 筛选类型
const filterType = ref<"all" | "draft" | "submitted">("all");
// 用户ID从登录状态获取
const userId = ref(0);
// 初始化
onMounted(async () => {
console.log("【日报列表】页面加载, user.info:", user.info.value);
// 先尝试获取最新用户信息
if (user.token) {
try {
await user.get();
} catch (e) {
console.error("【日报列表】获取用户信息失败:", e);
}
}
console.log("【日报列表】获取用户信息后, user.info:", user.info.value);
if (user.info.value && user.info.value.id) {
userId.value = user.info.value.id;
console.log("【日报列表】设置userId:", userId.value);
loadReports();
} else {
console.error("【日报列表】用户未登录或用户信息为空");
ui.showToast({
message: "请先登录",
type: "error"
});
setTimeout(() => {
router.to("/pages/user/login");
}, 1000);
}
});
// 加载日报列表
async function loadReports() {
if (loading.value || finished.value) return;
loading.value = true;
try {
console.log("【日报列表】开始加载, userId:", userId.value);
if (!userId.value || userId.value === 0) {
console.error("【日报列表】userId无效:", userId.value);
throw new Error("用户ID无效请重新登录");
}
const params: any = {
userId: userId.value,
page: page.value,
size: pageSize.value
};
// 添加状态筛选
if (filterType.value === "draft") {
params.status = 0;
} else if (filterType.value === "submitted") {
params.status = 1;
}
console.log("【日报列表】请求参数:", params);
const res = await request({
url: "/app/dailyreport/report/myReports",
method: "GET",
params
});
console.log("【日报列表】响应数据:", res);
console.log("【日报列表】响应类型:", typeof res);
console.log("【日报列表】是否为数组:", Array.isArray(res));
console.log("【日报列表】数据长度:", res ? res.length : 0);
if (res && res.length > 0) {
console.log("【日报列表】开始填充列表数据");
if (page.value === 1) {
list.value = res;
} else {
list.value.push(...res);
}
page.value++;
console.log("【日报列表】列表已更新, 当前list长度:", list.value.length);
console.log("【日报列表】列表第一项数据:", list.value[0]);
// 如果返回数据少于每页大小,说明已经到底了
if (res.length < pageSize.value) {
finished.value = true;
}
} else {
console.log("【日报列表】无数据或数据为空");
// 即使没有数据也不应该标记为finished可能只是筛选条件没有匹配的数据
if (page.value === 1) {
list.value = [];
}
finished.value = true;
}
} catch (error: any) {
console.error("加载日报列表失败:", error);
ui.showToast({
message: "加载失败: " + (error.message || "未知错误"),
type: "error"
});
} finally {
loading.value = false;
}
}
// 下拉刷新
function onRefresh() {
console.log("【日报列表】下拉刷新");
page.value = 1;
finished.value = false;
list.value = [];
loadReports();
}
// 切换筛选条件
function changeFilter(type: "all" | "draft" | "submitted") {
console.log("【日报列表】切换筛选:", type);
filterType.value = type;
page.value = 1;
finished.value = false;
list.value = [];
loadReports();
}
// 跳转到详情
function toDetail(item: any) {
const date = item.reportDate ? encodeURIComponent(item.reportDate) : '';
router.to(`/pages/dailyreport/detail?id=${item.id}&date=${date}`);
}
// 跳转到提交页面
function toSubmit() {
router.to("/pages/dailyreport/submit");
}
// 格式化日期
function formatDate(dateStr: string) {
if (!dateStr) return "";
const date = new Date(dateStr);
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${month}月${day}日`;
}
// 格式化星期
function formatWeekday(dateStr: string) {
if (!dateStr) return "";
const date = new Date(dateStr);
const weekdays = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];
return weekdays[date.getDay()];
}
// 格式化时间
function formatTime(timeStr: string) {
if (!timeStr) return "";
const date = new Date(timeStr);
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
return `${month}-${day} ${hours}:${minutes}`;
}
// 获取内容摘要
function getContentPreview(content: string) {
if (!content) return "(无内容)";
// 移除 Markdown 标记
const plain = content.replace(/[#*`\[\]()]/g, "").trim();
// 限制长度
return plain.length > 100 ? plain.substring(0, 100) + "..." : plain;
}
</script>
<style scoped>
.gap-2 {
gap: 0.5rem;
}
.line-clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
</style>

View File

@@ -0,0 +1,555 @@
<template>
<cl-page title="提交日报">
<view class="p-4">
<!-- 日期选择 -->
<view class="mb-4">
<text class="text-base font-bold mb-2">日报日期</text>
<cl-input
v-model="form.reportDate"
type="date"
placeholder="选择日报日期"
:max="todayDate"
/>
</view>
<!-- 输入方式选择 -->
<view class="mb-4">
<text class="text-base font-bold mb-2">输入方式</text>
<view class="flex gap-2">
<cl-button
:type="inputType === 0 ? 'primary' : 'default'"
size="small"
@tap="inputType = 0"
>
文字输入
</cl-button>
<cl-button
:type="inputType === 1 ? 'primary' : 'default'"
size="small"
@tap="inputType = 1"
>
语音输入
</cl-button>
</view>
</view>
<!-- 语音输入区域 -->
<view v-if="inputType === 1" class="mb-4">
<text class="text-base font-bold mb-2">语音录制</text>
<view class="flex flex-col items-center p-6 bg-gray-50 rounded-lg">
<view v-if="!isRecording && !audioFile" class="flex flex-col items-center">
<text class="text-gray-500 mb-4">按住按钮开始录音最长60秒</text>
<cl-button
type="primary"
size="large"
round
@touchstart="startRecording"
@touchend="stopRecording"
>
<text class="text-2xl">🎤</text>
</cl-button>
</view>
<view v-if="isRecording" class="flex flex-col items-center">
<text class="text-red-500 text-lg mb-2">录音中...</text>
<text class="text-3xl font-bold text-red-500 mb-4">{{ recordingTime }}s</text>
<cl-button type="danger" @tap="cancelRecording">取消录音</cl-button>
</view>
<view v-if="audioFile && !isRecording" class="flex flex-col items-center w-full">
<text class="text-green-500 mb-2">录音完成</text>
<text class="text-gray-600 mb-4">时长: {{ recordingTime }}秒</text>
<view class="flex gap-2">
<cl-button type="default" @tap="reRecord">重新录音</cl-button>
<cl-button type="primary" @tap="recognizeVoice" :loading="isRecognizing">
{{ isRecognizing ? '识别中...' : '语音识别' }}
</cl-button>
</view>
</view>
</view>
<!-- 识别结果 -->
<view v-if="recognizedText" class="mt-4">
<text class="text-base font-bold mb-2">识别结果</text>
<view class="p-3 bg-blue-50 rounded-lg">
<text class="text-gray-700">{{ recognizedText }}</text>
</view>
</view>
</view>
<!-- 文字输入区域 -->
<view v-if="inputType === 0" class="mb-4">
<view class="p-4 bg-white rounded-2xl">
<cl-form>
<cl-form-item label="工作内容" required>
<cl-input
v-model="originalText"
placeholder="请输入今天的工作内容"
:maxlength="1000"
/>
</cl-form-item>
</cl-form>
</view>
</view>
<!-- AI格式化按钮 -->
<view v-if="originalText || recognizedText" class="mb-4">
<cl-button
type="success"
size="large"
block
@tap="formatWithAI"
:loading="isFormatting"
>
{{ isFormatting ? 'AI生成中...' : '🤖 AI生成日报' }}
</cl-button>
</view>
<!-- AI生成的日报内容 -->
<view v-if="aiFormattedContent" class="mb-4">
<text class="text-base font-bold mb-2">AI生成的日报</text>
<view class="p-3 bg-green-50 rounded-lg mb-2">
<cl-markdown :content="aiFormattedContent" />
</view>
<text class="text-sm text-gray-500">您可以在下方编辑最终内容</text>
</view>
<!-- 最终编辑区域 -->
<view v-if="aiFormattedContent" class="mb-4">
<view class="p-4 bg-white rounded-2xl">
<cl-form>
<cl-form-item label="最终日报内容" required>
<cl-input
v-model="userEditedContent"
placeholder="请编辑最终的日报内容"
:maxlength="2000"
/>
</cl-form-item>
</cl-form>
</view>
</view>
<!-- 操作按钮 -->
<view v-if="userEditedContent" class="flex gap-2 mt-6">
<cl-button
type="default"
size="large"
:flex="1"
@tap="saveDraft"
:loading="isSavingDraft"
>
{{ isSavingDraft ? '保存中...' : '保存草稿' }}
</cl-button>
<cl-button
type="primary"
size="large"
:flex="1"
@tap="submitReport"
:loading="isSubmitting"
>
{{ isSubmitting ? '提交中...' : '提交日报' }}
</cl-button>
</view>
</view>
</cl-page>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from "vue";
import { request, router, useStore } from "@/cool";
import { useUi } from "@/uni_modules/cool-ui";
const ui = useUi();
const { user } = useStore();
// 表单数据
const form = ref({
reportDate: "",
userId: 0 // 将在onMounted中从登录用户信息获取
});
// 输入方式0-文字1-语音
const inputType = ref<number>(0);
// 语音录制相关
const isRecording = ref(false);
const recordingTime = ref(0);
const audioFile = ref<string | null>(null);
const recorderManager = ref<any>(null);
let recordTimer: number | null = null;
// 识别和格式化相关
const isRecognizing = ref(false);
const recognizedText = ref("");
const originalText = ref("");
const isFormatting = ref(false);
const aiFormattedContent = ref("");
const userEditedContent = ref("");
// 提交相关
const isSavingDraft = ref(false);
const isSubmitting = ref(false);
// 今天的日期
const todayDate = computed(() => {
const today = new Date();
return `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}-${String(today.getDate()).padStart(2, "0")}`;
});
onMounted(async () => {
console.log("【日报提交】页面加载, user.info:", user.info.value);
// 先尝试获取最新用户信息
if (user.token) {
try {
await user.get();
} catch (e) {
console.error("【日报提交】获取用户信息失败:", e);
}
}
console.log("【日报提交】获取用户信息后, user.info:", user.info.value);
// 获取当前登录用户ID
if (user.info.value && user.info.value.id) {
form.value.userId = user.info.value.id;
console.log("【日报提交】设置userId:", form.value.userId);
} else {
// 如果未登录,跳转到登录页
console.error("【日报提交】用户未登录或用户信息为空");
ui.showToast({
message: "请先登录",
type: "error"
});
setTimeout(() => {
router.to("/pages/user/login");
}, 1000);
return;
}
// 设置默认日期为今天
form.value.reportDate = todayDate.value;
// 初始化录音管理器(仅在支持的平台)
try {
// @ts-ignore
const manager = uni.getRecorderManager();
if (manager) {
recorderManager.value = manager;
// 监听录音结束
recorderManager.value.onStop((res: any) => {
if (recordTimer) {
clearInterval(recordTimer);
recordTimer = null;
}
audioFile.value = res.tempFilePath;
isRecording.value = false;
});
// 监听录音错误
recorderManager.value.onError((err: any) => {
console.error("录音错误:", err);
ui.showToast({
message: "录音失败: " + err.errMsg,
type: "error"
});
isRecording.value = false;
if (recordTimer) {
clearInterval(recordTimer);
recordTimer = null;
}
});
console.log("录音管理器初始化成功");
} else {
console.warn("当前环境不支持录音功能H5环境请使用文字输入");
}
} catch (e) {
console.warn("录音管理器初始化失败:", e);
}
// 检查今天是否已有日报
checkTodayReport();
});
onUnmounted(() => {
if (recordTimer) {
clearInterval(recordTimer);
}
});
// 检查今天是否已有日报
async function checkTodayReport() {
try {
const res = await request({
url: "/app/dailyreport/report/todayReport",
method: "GET",
params: { userId: form.value.userId }
});
if (res && res.id) {
// 今天已有日报,询问是否继续编辑
uni.showModal({
title: "提示",
content: `您今天已经${res.status === 1 ? "提交" : "保存"}了日报,是否继续编辑?`,
success: (modalRes) => {
if (modalRes.confirm) {
// 加载已有日报
loadExistingReport(res);
} else {
// 跳转到列表页
router.back();
}
}
});
}
} catch (error) {
console.log("今天还没有日报,可以新建");
}
}
// 加载已有日报
function loadExistingReport(report: any) {
originalText.value = report.originalText || "";
aiFormattedContent.value = report.aiFormattedContent || "";
userEditedContent.value = report.userEditedContent || "";
inputType.value = report.inputType || 0;
}
// 开始录音
function startRecording() {
// 检查录音管理器是否可用
if (!recorderManager.value) {
return ui.showToast({
message: "当前环境不支持录音功能,请使用文字输入",
type: "error"
});
}
isRecording.value = true;
recordingTime.value = 0;
audioFile.value = null;
recognizedText.value = "";
// 开始录音
recorderManager.value.start({
duration: 60000, // 最长60秒
format: "mp3"
});
// 开始计时
recordTimer = setInterval(() => {
recordingTime.value++;
if (recordingTime.value >= 60) {
stopRecording();
}
}, 1000);
}
// 停止录音
function stopRecording() {
if (isRecording.value && recorderManager.value) {
recorderManager.value.stop();
}
}
// 取消录音
function cancelRecording() {
if (isRecording.value && recorderManager.value) {
recorderManager.value.stop();
audioFile.value = null;
recordingTime.value = 0;
}
}
// 重新录音
function reRecord() {
audioFile.value = null;
recognizedText.value = "";
recordingTime.value = 0;
}
// 语音识别
async function recognizeVoice() {
if (!audioFile.value) {
return ui.showToast({
message: "请先录音",
type: "error"
});
}
isRecognizing.value = true;
try {
// 上传音频文件
const uploadRes = await uni.uploadFile({
url: request.options.baseURL + "/app/dailyreport/report/uploadVoice",
filePath: audioFile.value,
name: "audio",
formData: {
userId: form.value.userId
}
});
const result = JSON.parse(uploadRes.data);
if (result.code === 1000) {
recognizedText.value = result.data;
originalText.value = result.data;
ui.showToast({
message: "识别成功",
type: "success"
});
} else {
throw new Error(result.message || "识别失败");
}
} catch (error: any) {
console.error("语音识别失败:", error);
ui.showToast({
message: "语音识别失败: " + (error.message || "未知错误"),
type: "error"
});
} finally {
isRecognizing.value = false;
}
}
// AI格式化
async function formatWithAI() {
const text = originalText.value || recognizedText.value;
if (!text) {
return ui.showToast({
message: "请先输入内容或录音",
type: "error"
});
}
isFormatting.value = true;
try {
const res = await request({
url: "/app/dailyreport/report/aiFormat",
method: "POST",
data: {
originalText: text,
reportDate: form.value.reportDate // 传递日报日期
}
});
// 后端返回的是对象 {formattedContent: "内容", length: 数字}
const formattedContent = res.formattedContent || res;
aiFormattedContent.value = formattedContent;
userEditedContent.value = formattedContent;
ui.showToast({
message: "AI生成成功",
type: "success"
});
} catch (error: any) {
console.error("AI格式化失败:", error);
ui.showToast({
message: "AI格式化失败: " + (error.message || "未知错误"),
type: "error"
});
} finally {
isFormatting.value = false;
}
}
// 保存草稿
async function saveDraft() {
if (!userEditedContent.value) {
return ui.showToast({
message: "请先生成日报内容",
type: "error"
});
}
isSavingDraft.value = true;
try {
await request({
url: "/app/dailyreport/report/saveDraft",
method: "POST",
data: {
userId: form.value.userId,
reportDate: form.value.reportDate,
originalText: originalText.value || recognizedText.value,
aiFormattedContent: aiFormattedContent.value,
userEditedContent: userEditedContent.value,
inputType: inputType.value
}
});
ui.showToast({
message: "草稿保存成功",
type: "success"
});
// 延迟跳转
setTimeout(() => {
router.back();
}, 1000);
} catch (error: any) {
console.error("保存草稿失败:", error);
ui.showToast({
message: "保存草稿失败: " + (error.message || "未知错误"),
type: "error"
});
} finally {
isSavingDraft.value = false;
}
}
// 提交日报
async function submitReport() {
if (!userEditedContent.value) {
return ui.showToast({
message: "请先生成日报内容",
type: "error"
});
}
isSubmitting.value = true;
try {
await request({
url: "/app/dailyreport/report/submit",
method: "POST",
data: {
userId: form.value.userId,
reportDate: form.value.reportDate,
originalText: originalText.value || recognizedText.value,
aiFormattedContent: aiFormattedContent.value,
userEditedContent: userEditedContent.value,
inputType: inputType.value
}
});
ui.showToast({
message: "日报提交成功",
type: "success"
});
// 延迟跳转
setTimeout(() => {
router.back();
}, 1000);
} catch (error: any) {
console.error("提交日报失败:", error);
ui.showToast({
message: "提交日报失败: " + (error.message || "未知错误"),
type: "error"
});
} finally {
isSubmitting.value = false;
}
}
</script>
<style scoped>
.gap-2 {
gap: 0.5rem;
}
</style>

View File

@@ -0,0 +1,133 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<cl-button>{{ t("普通") }}</cl-button>
</demo-item>
<demo-item :label="t('不同类型')">
<view class="flex flex-row flex-wrap mb-2 overflow-visible">
<cl-button type="primary">{{ t("主要") }}</cl-button>
<cl-button type="success">{{ t("成功") }}</cl-button>
<cl-button type="warn">{{ t("警告") }}</cl-button>
</view>
<view class="flex flex-row mb-2 overflow-visible">
<cl-button type="error">{{ t("危险") }}</cl-button>
<cl-button type="info">{{ t("信息") }}</cl-button>
</view>
<view class="flex flex-row overflow-visible">
<cl-button type="light">{{ t("浅色") }}</cl-button>
<cl-button type="dark">{{ t("深色") }}</cl-button>
</view>
</demo-item>
<demo-item :label="t('只显示图标')">
<view class="flex flex-row">
<cl-button type="primary" icon="send-plane-fill"></cl-button>
<cl-button type="error" icon="verified-badge-fill"></cl-button>
<cl-button type="info" icon="edit-fill"></cl-button>
</view>
</demo-item>
<demo-item :label="t('自定义')">
<view class="flex flex-row justify-center mb-5 h-14 items-center">
<cl-button
:type="type"
:size="size"
:text="isText"
:border="isBorder"
:rounded="isRounded"
:loading="isLoading"
:disabled="isDisabled"
:icon="isIcon ? 'send-plane-fill' : ''"
:color="isColor ? '#4286e0' : ''"
:pt="{
className: parseClass([
{
'!bg-transparent': isColor
}
])
}"
>{{ t("自定义") }}</cl-button
>
</view>
<cl-list border>
<view class="p-2">
<cl-tabs
v-model="size"
:height="66"
:list="sizeOptions"
show-slider
fill
></cl-tabs>
</view>
<cl-list-item :label="t('文本模式')">
<cl-switch v-model="isText"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('带边框')">
<cl-switch v-model="isBorder"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('圆角按钮')">
<cl-switch v-model="isRounded"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('带左侧图标')">
<cl-switch v-model="isIcon"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('加载中')">
<cl-switch v-model="isLoading"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('禁用')">
<cl-switch v-model="isDisabled"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('自定义颜色')">
<cl-switch v-model="isColor"></cl-switch>
</cl-list-item>
</cl-list>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import DemoItem from "../components/item.uvue";
import type { ClButtonType, ClTabsItem, Size } from "@/uni_modules/cool-ui";
import { parseClass } from "@/cool";
import { t } from "@/locale";
const type = ref<ClButtonType>("primary");
const isText = ref(false);
const isBorder = ref(false);
const isRounded = ref(false);
const isLoading = ref(false);
const isIcon = ref(false);
const isDisabled = ref(false);
const isColor = ref(false);
const size = ref<Size>("normal");
const sizeOptions = ref<ClTabsItem[]>([
{
label: t("小"),
value: "small"
},
{
label: t("默认"),
value: "normal"
},
{
label: t("大"),
value: "large"
}
]);
</script>

View File

@@ -0,0 +1,94 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('设置颜色')">
<view class="flex flex-row">
<cl-icon name="heart-fill" color="primary" class="mr-2"></cl-icon>
<cl-icon name="heart-fill" color="success" class="mr-2"></cl-icon>
<cl-icon name="heart-fill" color="error" class="mr-2"></cl-icon>
<cl-icon name="heart-fill" color="warn" class="mr-2"></cl-icon>
<cl-icon name="heart-fill" color="info" class="mr-2"></cl-icon>
<cl-icon name="heart-fill" color="#428bca" class="mr-2"></cl-icon>
<cl-icon name="heart-fill" color="purple"></cl-icon>
</view>
</demo-item>
<demo-item :label="t('设置大小')">
<view class="flex flex-row">
<cl-icon name="heart-fill" class="mr-2" :size="50"></cl-icon>
<cl-icon name="heart-fill" class="mr-2" :size="40"></cl-icon>
<cl-icon name="heart-fill" class="mr-2" :size="30"></cl-icon>
<cl-icon name="heart-fill" class="mr-2" :size="20"></cl-icon>
</view>
</demo-item>
<demo-item>
<cl-text>{{ t("集成 iconfont 与 remixicon 图标库,展示部分示例") }}</cl-text>
</demo-item>
<demo-item :label="t('iconfont')">
<cl-row :gutter="10">
<cl-col :span="4" v-for="item in iconfont" :key="item">
<view
class="flex flex-col items-center justify-center h-[100rpx] rounded-lg"
hover-class="opacity-60"
:hover-stay-time="250"
@tap="copy(item)"
>
<cl-icon :name="item"></cl-icon>
</view>
</cl-col>
</cl-row>
</demo-item>
<demo-item :label="t('remixicon')">
<cl-row :gutter="10">
<cl-col :span="4" v-for="item in remixicon" :key="item">
<view
class="flex flex-col items-center justify-center h-[100rpx]"
hover-class="opacity-60"
:hover-stay-time="250"
@tap="copy(item)"
>
<cl-icon :name="item"></cl-icon>
</view>
</cl-col>
</cl-row>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import DemoItem from "../components/item.uvue";
import { icons } from "@/icons";
import { forInObject, keys } from "@/cool";
import { useUi } from "@/uni_modules/cool-ui";
import { t } from "@/locale";
const ui = useUi();
const remixicon = ref<string[]>([]);
const iconfont = ref<string[]>([]);
forInObject(icons, (value, key) => {
if (key == "iconfont") {
iconfont.value = keys(value).slice(0, 100);
} else {
remixicon.value = keys(value).slice(0, 100);
}
});
function copy(data: string) {
uni.setClipboardData({
data,
showToast: false,
success() {
ui.showToast({
message: t("复制成功")
});
}
});
}
</script>

View File

@@ -0,0 +1,116 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<cl-image :src="url"></cl-image>
</demo-item>
<demo-item :label="t('不同裁剪')">
<view class="flex flex-row justify-between">
<view class="flex flex-col items-center justify-center">
<cl-image :src="url" mode="aspectFill"></cl-image>
<cl-text
:pt="{
className: 'text-sm mt-1'
}"
>aspectFill</cl-text
>
</view>
<view class="flex flex-col items-center justify-center">
<cl-image :src="url" mode="aspectFit"></cl-image>
<cl-text
:pt="{
className: 'text-sm mt-1'
}"
>aspectFit</cl-text
>
</view>
<view class="flex flex-col items-center justify-center">
<cl-image :src="url" mode="heightFix"></cl-image>
<cl-text
:pt="{
className: 'text-sm mt-1'
}"
>heightFix</cl-text
>
</view>
<view class="flex flex-col items-center justify-center">
<cl-image :src="url" mode="scaleToFill"></cl-image>
<cl-text
:pt="{
className: 'text-sm mt-1'
}"
>scaleToFill</cl-text
>
</view>
</view>
</demo-item>
<demo-item :label="t('点击可预览')">
<cl-image :src="url" preview></cl-image>
</demo-item>
<demo-item :label="t('失败时显示')">
<cl-image src="url"></cl-image>
</demo-item>
<demo-item :label="t('加载中')">
<cl-image src=""></cl-image>
</demo-item>
<demo-item :label="t('自定义圆角')">
<view class="flex flex-row">
<cl-image
:src="url"
:pt="{
inner: {
className: '!rounded-none'
}
}"
></cl-image>
<cl-image
:src="url"
:pt="{
className: 'ml-3',
inner: {
className: '!rounded-2xl'
}
}"
></cl-image>
<cl-image
:src="url"
:pt="{
className: 'ml-3',
inner: {
className: '!rounded-3xl'
}
}"
></cl-image>
<cl-image
:src="url"
:pt="{
className: 'ml-3',
inner: {
className: '!rounded-full'
}
}"
></cl-image>
</view>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import DemoItem from "../components/item.uvue";
import { t } from "@/locale";
const url = ref("https://unix.cool-js.com/images/demo/avatar.jpg");
</script>

View File

@@ -0,0 +1,72 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<view class="flex flex-row">
<cl-tag>{{ t("标签") }}</cl-tag>
</view>
</demo-item>
<demo-item :label="t('不同类型')">
<view class="flex flex-row flex-wrap">
<cl-tag type="primary">{{ t("主要") }}</cl-tag>
<cl-tag type="success">{{ t("成功") }}</cl-tag>
<cl-tag type="warn">{{ t("警告") }}</cl-tag>
<cl-tag type="error">{{ t("危险") }}</cl-tag>
<cl-tag type="info">{{ t("信息") }}</cl-tag>
</view>
</demo-item>
<demo-item :label="t('带图标')">
<view class="flex flex-row">
<cl-tag icon="mail-line">{{ t("邮件") }}</cl-tag>
<cl-tag icon="calendar-line">{{ t("日历") }}</cl-tag>
<cl-tag icon="file-line">{{ t("文件") }}</cl-tag>
</view>
</demo-item>
<demo-item :label="t('圆角')">
<view class="flex flex-row">
<cl-tag rounded>{{ t("圆角") }}</cl-tag>
</view>
</demo-item>
<demo-item :label="t('可关闭')">
<view class="flex flex-row">
<cl-tag closable>{{ t("可关闭") }}</cl-tag>
</view>
</demo-item>
<demo-item :label="t('镂空')">
<view class="flex flex-row flex-wrap">
<cl-tag type="primary" plain>{{ t("主要") }}</cl-tag>
<cl-tag type="success" plain>{{ t("成功") }}</cl-tag>
<cl-tag type="warn" plain>{{ t("警告") }}</cl-tag>
<cl-tag type="error" plain>{{ t("危险") }}</cl-tag>
<cl-tag type="info" plain>{{ t("信息") }}</cl-tag>
</view>
</demo-item>
<demo-item :label="t('自定义')">
<view class="flex flex-row">
<cl-tag
:pt="{
className: '!bg-sky-200',
text: {
className: '!text-sky-700'
}
}"
>{{ t("自定义颜色") }}</cl-tag
>
<cl-tag :pt="{ className: '!rounded-none' }">{{ t("自定义无圆角") }}</cl-tag>
</view>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import DemoItem from "../components/item.uvue";
import { t } from "@/locale";
</script>

View File

@@ -0,0 +1,103 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<cl-text>云想衣裳花想容,春风拂槛露华浓。</cl-text>
</demo-item>
<demo-item :label="t('自定义颜色')">
<cl-text color="primary">明月松间照,清泉石上流。</cl-text>
<cl-text color="error">举头望明月,低头思故乡。</cl-text>
<cl-text color="success">春眠不觉晓,处处闻啼鸟。</cl-text>
<cl-text color="warn">劝君更尽一杯酒,西出阳关无故人。</cl-text>
<cl-text color="info">孤帆远影碧空尽,唯见长江天际流。</cl-text>
<cl-text
:pt="{
className: 'text-sky-500'
}"
>大漠孤烟直,长河落日圆。</cl-text
>
</demo-item>
<demo-item :label="t('省略号')">
<cl-text ellipsis
>云想衣裳花想容,春风拂槛露华浓。若非群玉山头见,会向瑶台月下逢。</cl-text
>
</demo-item>
<demo-item :label="t('多行省略号')">
<cl-text ellipsis :lines="2"
>云想衣裳花想容,春风拂槛露华浓。若非群玉山头见,会向瑶台月下逢。天阶夜色凉如水,卧看牵牛织女星。人生若只如初见,何事秋风悲画扇。山有木兮木有枝,心悦君兮君不知。</cl-text
>
</demo-item>
<demo-item :label="t('字体大小')">
<cl-text
:pt="{
className: 'text-xs'
}"
>text-xs</cl-text
>
<cl-text
:pt="{
className: 'text-sm'
}"
>text-sm</cl-text
>
<cl-text
:pt="{
className: 'text-md'
}"
>text-md</cl-text
>
<cl-text
:pt="{
className: 'text-lg'
}"
>text-lg</cl-text
>
<cl-text
:pt="{
className: 'text-xl'
}"
>text-xl</cl-text
>
</demo-item>
<demo-item :label="t('自定义尺寸')">
<cl-text :size="20">20rpx</cl-text>
<cl-text size="30rpx">30rpx</cl-text>
<cl-text size="15px">15px</cl-text>
</demo-item>
<demo-item :label="t('金额')">
<cl-text type="amount" :value="10000000.0"></cl-text>
</demo-item>
<demo-item :label="t('手机号脱敏')">
<cl-text type="phone" mask value="13800138000"></cl-text>
</demo-item>
<demo-item :label="t('姓名脱敏')">
<cl-text type="name" mask value="张三"></cl-text>
</demo-item>
<demo-item :label="t('邮箱脱敏')">
<cl-text type="email" mask value="example@example.com"></cl-text>
</demo-item>
<demo-item :label="t('银行卡脱敏')">
<cl-text type="card" mask value="1234 5678 9012 3456"></cl-text>
</demo-item>
<demo-item :label="t('自定义脱敏字符')">
<cl-text type="phone" value="13800138000" mask mask-char="~"></cl-text>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { t } from "@/locale";
import DemoItem from "../components/item.uvue";
</script>

View File

@@ -0,0 +1,32 @@
<template>
<view class="p-3 pb-0">
<view class="w-full p-3 bg-white rounded-xl dark:bg-surface-800">
<cl-image :src="item?.image" mode="aspectFill" width="100%" height="280rpx"></cl-image>
<cl-text :pt="{ className: 'mt-2' }">{{ item?.title }}</cl-text>
</view>
</view>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import { parse } from "@/cool";
defineOptions({
name: "goods-item"
});
type GoodsItem = {
id: number;
title: string;
image: string;
};
const props = defineProps({
value: {
type: Object,
default: () => ({})
}
});
const item = computed(() => parse<GoodsItem>(props.value));
</script>

View File

@@ -0,0 +1,28 @@
<template>
<view class="demo-item dark:!bg-surface-800">
<cl-text :pt="{ className: 'text-sm text-surface-400 mb-2' }" v-if="label != ''">{{
label
}}</cl-text>
<slot></slot>
</view>
</template>
<script lang="ts" setup>
defineOptions({
name: "demo-item"
});
const props = defineProps({
label: {
type: String,
default: ""
}
});
</script>
<style lang="scss" scoped>
.demo-item {
@apply p-3 rounded-xl bg-white mb-3;
}
</style>

View File

@@ -0,0 +1,17 @@
<template>
<view class="bg-surface-100 dark:!bg-surface-700 rounded-lg p-3 mb-3">
<cl-text
:pt="{
className: 'text-sm'
}"
>
<slot></slot>
</cl-text>
</view>
</template>
<script setup lang="ts">
defineOptions({
name: "demo-tips"
});
</script>

View File

@@ -0,0 +1,32 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<cl-avatar src="https://unix.cool-js.com/images/demo/avatar.jpg"></cl-avatar>
</demo-item>
<demo-item :label="t('无图片')">
<cl-avatar></cl-avatar>
</demo-item>
<demo-item :label="t('圆角')">
<cl-avatar
rounded
src="https://unix.cool-js.com/images/demo/avatar.jpg"
></cl-avatar>
</demo-item>
<demo-item :label="t('自定义大小')">
<cl-avatar
:size="120"
src="https://unix.cool-js.com/images/demo/avatar.jpg"
></cl-avatar>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { t } from "@/locale";
import DemoItem from "../components/item.uvue";
</script>

View File

@@ -0,0 +1,59 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<cl-banner :list="list"></cl-banner>
</demo-item>
<demo-item :label="t('禁用手势')">
<cl-banner :list="list" :disable-touch="true"></cl-banner>
</demo-item>
<demo-item :label="t('自定义样式')">
<cl-banner
:list="list"
:pt="{
className: 'mx-[-12rpx] !rounded-none',
item: {
className: parseClass(['scale-y-80 !px-[12rpx]'])
},
itemActive: {
className: parseClass(['!scale-y-100'])
},
image: {
className: '!rounded-none'
}
}"
:previous-margin="40"
:next-margin="40"
></cl-banner>
</demo-item>
<demo-item :label="t('自定义样式2')">
<cl-banner
:list="list"
:pt="{
className: 'mx-[-12rpx]',
item: {
className: parseClass(['px-[12rpx]'])
}
}"
:next-margin="40"
></cl-banner>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { t } from "@/locale";
import DemoItem from "../components/item.uvue";
import { ref } from "vue";
import { parseClass } from "@/cool";
const list = ref<string[]>([
"https://unix.cool-js.com/images/demo/bg1.png",
"https://unix.cool-js.com/images/demo/bg2.png",
"https://unix.cool-js.com/images/demo/bg3.png"
]);
</script>

View File

@@ -0,0 +1,217 @@
<template>
<cl-page>
<view class="p-3">
<demo-item>
<cl-text color="info">
{{ t("长按项即可拖动排序") }}
</cl-text>
</demo-item>
<demo-item :label="t('单列排序')">
<cl-draggable v-model="list">
<template #item="{ item, index }">
<view
class="flex flex-row items-center p-3 bg-surface-100 rounded-lg mb-2 dark:!bg-surface-700"
:class="{
'opacity-50': item['disabled']
}"
>
<cl-text>{{ (item as UTSJSONObject).label }}</cl-text>
</view>
</template>
</cl-draggable>
</demo-item>
<demo-item :label="t('不需要长按')">
<cl-draggable v-model="list5" :long-press="false">
<template #item="{ item }">
<view
class="flex flex-row items-center p-3 bg-surface-100 rounded-lg mb-2 dark:!bg-surface-700"
>
<cl-text>{{ (item as UTSJSONObject).label }}</cl-text>
</view>
</template>
</cl-draggable>
</demo-item>
<demo-item :label="t('结合列表使用')">
<cl-list border>
<cl-draggable v-model="list2">
<template #item="{ item, index, dragging, dragIndex }">
<cl-list-item
icon="chat-thread-line"
:label="(item as UTSJSONObject).label"
arrow
:pt="{
inner: {
className: parseClass([
[
dragging && dragIndex == index,
isDark ? '!bg-surface-700' : '!bg-surface-100'
]
])
}
}"
></cl-list-item>
</template>
</cl-draggable>
</cl-list>
</demo-item>
<demo-item :label="t('多列排序')">
<cl-draggable v-model="list3" :columns="4">
<template #item="{ item, index }">
<view
class="flex flex-row items-center justify-center p-3 bg-surface-100 rounded-lg m-1 dark:!bg-surface-700"
:class="{
'opacity-50': item['disabled']
}"
>
<cl-text>{{ (item as UTSJSONObject).label }}</cl-text>
</view>
</template>
</cl-draggable>
</demo-item>
<demo-item :label="t('结合图片使用')">
<cl-draggable v-model="list4" :columns="4">
<template #item="{ item, index }">
<view class="p-[2px]">
<cl-image
:src="(item as UTSJSONObject).url"
mode="widthFix"
:pt="{
className: '!w-full'
}"
preview
></cl-image>
</view>
</template>
</cl-draggable>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { t } from "@/locale";
import DemoItem from "../components/item.uvue";
import { ref } from "vue";
import { isDark, parseClass } from "@/cool";
// list李白《将进酒》
const list = ref<UTSJSONObject[]>([
{
label: "君不见黄河之水天上来"
},
{
label: "奔流到海不复回",
disabled: true
},
{
label: "君不见高堂明镜悲白发"
},
{
label: "朝如青丝暮成雪"
},
{
label: "人生得意须尽欢"
}
]);
// list5杜甫《春望》
const list5 = ref<UTSJSONObject[]>([
{
label: "国破山河在"
},
{
label: "城春草木深"
},
{
label: "感时花溅泪"
}
]);
// list2王之涣《登鹳雀楼》
const list2 = ref<UTSJSONObject[]>([
{
label: "白日依山尽"
},
{
label: "黄河入海流"
},
{
label: "欲穷千里目"
},
{
label: "更上一层楼"
},
{
label: "一览众山小"
}
]);
const list3 = ref<UTSJSONObject[]>([
{
label: "项目1"
},
{
label: "项目2"
},
{
label: "项目3"
},
{
label: "项目4"
},
{
label: "项目5"
},
{
label: "项目6"
},
{
label: "项目7"
},
{
label: "项目8",
disabled: true
},
{
label: "项目9"
},
{
label: "项目10"
},
{
label: "项目11"
},
{
label: "项目12"
}
]);
const list4 = ref<UTSJSONObject[]>([
{
url: "https://unix.cool-js.com/images/demo/1.jpg"
},
{
url: "https://unix.cool-js.com/images/demo/2.jpg"
},
{
url: "https://unix.cool-js.com/images/demo/3.jpg"
},
{
url: "https://unix.cool-js.com/images/demo/4.jpg"
},
{
url: "https://unix.cool-js.com/images/demo/5.jpg"
},
{
url: "https://unix.cool-js.com/images/demo/6.jpg"
},
{
url: "https://unix.cool-js.com/images/demo/7.jpg"
}
]);
</script>

View File

@@ -0,0 +1,467 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<cl-filter-bar>
<!-- 下拉框 -->
<cl-filter-item
label="综合排序"
type="select"
:value="1"
:options="coreOptions"
:pt="{
className: 'w-[220rpx] !flex-none'
}"
@change="onOptionsChange"
></cl-filter-item>
<!-- 排序 -->
<cl-filter-item
label="销量"
type="sort"
value="desc"
@change="onSortChange"
></cl-filter-item>
<!-- 开关 -->
<cl-filter-item
label="国补"
type="switch"
:value="false"
@change="onSwitchChange"
></cl-filter-item>
<!-- 自定义 -->
<view
class="flex flex-row items-center justify-center flex-1"
@tap="openFilter"
>
<cl-text>筛选</cl-text>
<cl-icon name="filter-line"></cl-icon>
</view>
</cl-filter-bar>
</demo-item>
<demo-item>
<cl-text pre-wrap :pt="{ className: 'text-sm p-2' }">{{
JSON.stringify(filterForm, null, 4)
}}</cl-text>
</demo-item>
<demo-item>
<cl-text pre-wrap :pt="{ className: 'text-sm p-2' }">{{
JSON.stringify(searchForm, null, 4)
}}</cl-text>
</demo-item>
</view>
<!-- 自定义筛选 -->
<cl-popup
v-model="filterVisible"
:title="t('筛选')"
direction="right"
size="80%"
:show-header="false"
>
<view class="flex flex-col h-full">
<scroll-view class="flex-1">
<cl-form :pt="{ className: 'p-3' }">
<cl-form-item label="服务/折扣">
<cl-row :gutter="20">
<cl-col :span="8" v-for="(item, index) in disOptions" :key="index">
<cl-checkbox
v-model="searchForm.dis"
:label="item.label"
:value="item.value"
:show-icon="false"
:pt="{
className: parseClass([
'mb-3 p-2 rounded-lg justify-center border border-solid border-transparent',
[isDark, 'bg-surface-800', 'bg-surface-100'],
[
searchForm.dis.includes(item.value),
`${isDark ? '!bg-surface-700' : '!bg-white'} !border-primary-500`
]
]),
label: {
className: 'text-sm'
}
}"
></cl-checkbox>
</cl-col>
</cl-row>
</cl-form-item>
<cl-form-item label="价格区间">
<view class="flex flex-row items-center">
<cl-input
v-model="searchForm.minPrice"
type="digit"
placeholder="最低价"
:pt="{
className: 'flex-1',
inner: {
className: 'text-center'
}
}"
></cl-input>
<cl-text
:pt="{
className: 'px-2'
}"
>~</cl-text
>
<cl-input
v-model="searchForm.maxPrice"
type="digit"
placeholder="最高价"
:pt="{
className: 'flex-1',
inner: {
className: 'text-center'
}
}"
></cl-input>
</view>
</cl-form-item>
<cl-form-item label="品牌">
<cl-row :gutter="20">
<cl-col
:span="8"
v-for="(item, index) in brandOptions"
:key="index"
>
<cl-checkbox
v-model="searchForm.brand"
:label="item.label"
:value="item.value"
:show-icon="false"
:pt="{
className: parseClass([
'mb-3 p-2 rounded-lg justify-center border border-solid border-transparent',
[isDark, 'bg-surface-800', 'bg-surface-100'],
[
searchForm.brand.includes(item.value),
`${isDark ? '!bg-surface-700' : '!bg-white'} !border-primary-500`
]
]),
label: {
className: 'text-sm'
}
}"
></cl-checkbox>
</cl-col>
</cl-row>
</cl-form-item>
<cl-form-item label="内存">
<cl-row :gutter="20">
<cl-col
:span="8"
v-for="(item, index) in memoryOptions"
:key="index"
>
<cl-radio
v-model="searchForm.memory"
:label="item.label"
:value="item.value"
:show-icon="false"
:pt="{
className: parseClass([
'mb-3 p-2 rounded-lg justify-center border border-solid border-transparent',
[isDark, 'bg-surface-800', 'bg-surface-100'],
[
searchForm.memory == item.value,
`${isDark ? '!bg-surface-700' : '!bg-white'} !border-primary-500`
]
]),
label: {
className: 'text-sm'
}
}"
></cl-radio>
</cl-col>
</cl-row>
</cl-form-item>
<cl-form-item label="颜色">
<cl-row :gutter="20">
<cl-col
:span="8"
v-for="(item, index) in colorOptions"
:key="index"
>
<cl-radio
v-model="searchForm.color"
:label="item.label"
:value="item.value"
:show-icon="false"
:pt="{
className: parseClass([
'mb-3 p-2 rounded-lg justify-center border border-solid border-transparent',
[isDark, 'bg-surface-800', 'bg-surface-100'],
[
searchForm.color == item.value,
`${isDark ? '!bg-surface-700' : '!bg-white'} !border-primary-500`
]
]),
label: {
className: 'text-sm'
}
}"
></cl-radio>
</cl-col>
</cl-row>
</cl-form-item>
</cl-form>
</scroll-view>
<view class="flex flex-row p-3">
<cl-button
type="info"
text
border
:pt="{
className: 'flex-1'
}"
@tap="closeFilter"
>{{ t("取消") }}</cl-button
>
<cl-button
:pt="{
className: 'flex-1'
}"
@tap="submit"
>{{ t("确定") }}</cl-button
>
</view>
<cl-safe-area type="bottom"></cl-safe-area>
</view>
</cl-popup>
</cl-page>
</template>
<script lang="ts" setup>
import { t } from "@/locale";
import DemoItem from "../components/item.uvue";
import { reactive, ref } from "vue";
import { useUi, type ClSelectOption } from "@/uni_modules/cool-ui";
import { isDark, parseClass } from "@/cool";
const ui = useUi();
const filterVisible = ref(false);
function openFilter() {
filterVisible.value = true;
}
function closeFilter() {
filterVisible.value = false;
}
function submit() {
closeFilter();
ui.showLoading();
setTimeout(() => {
ui.hideLoading();
}, 1000);
}
const coreOptions = ref<ClSelectOption[]>([
{
label: "综合排序",
value: 1
},
{
label: "价格从高到底",
value: 2
},
{
label: "价格从低到高",
value: 3
}
]);
type Option = {
label: string;
value: string;
};
const disOptions = ref<Option[]>([
{
label: "百亿补贴",
value: "billion_subsidy"
},
{
label: "以旧换新",
value: "trade_in"
},
{
label: "分期免息",
value: "installment"
},
{
label: "包邮",
value: "free_shipping"
},
{
label: "促销",
value: "promotion"
},
{
label: "价保",
value: "price_protection"
},
{
label: "仅看有货",
value: "in_stock"
},
{
label: "货到付款",
value: "cod"
}
]);
const brandOptions = ref<Option[]>([
{
label: "华为",
value: "huawei"
},
{
label: "苹果",
value: "apple"
},
{
label: "小米",
value: "xiaomi"
},
{
label: "三星",
value: "samsung"
},
{
label: "OPPO",
value: "oppo"
},
{
label: "vivo",
value: "vivo"
},
{
label: "荣耀",
value: "honor"
}
]);
const colorOptions = ref<Option[]>([
{
label: "红色",
value: "red"
},
{
label: "蓝色",
value: "blue"
},
{
label: "黑色",
value: "black"
},
{
label: "白色",
value: "white"
},
{
label: "金色",
value: "gold"
},
{
label: "银色",
value: "silver"
},
{
label: "绿色",
value: "green"
},
{
label: "紫色",
value: "purple"
},
{
label: "灰色",
value: "gray"
},
{
label: "粉色",
value: "pink"
}
]);
const memoryOptions = ref<Option[]>([
{
label: "128GB",
value: "128"
},
{
label: "256GB",
value: "256"
},
{
label: "512GB",
value: "512"
},
{
label: "1TB",
value: "1024"
}
]);
type SearchForm = {
dis: string[];
minPrice: string;
maxPrice: string;
brand: string[];
memory: string;
color: string;
};
const searchForm = ref<SearchForm>({
dis: [],
minPrice: "50",
maxPrice: "300",
brand: [],
memory: "",
color: ""
});
type FilterForm = {
core: number;
sort: string;
switch: boolean;
};
const filterForm = reactive<FilterForm>({
core: 0,
sort: "none",
switch: false
});
function onOptionsChange(val: number) {
console.log(val);
filterForm.core = val;
}
function onSortChange(val: string) {
console.log(val);
filterForm.sort = val;
}
function onSwitchChange(val: boolean) {
console.log(val);
filterForm.switch = val;
}
</script>

View File

@@ -0,0 +1,118 @@
<template>
<cl-page>
<cl-list-view
ref="listViewRef"
:data="listView"
:virtual="false"
:pt="{
refresher: {
className: 'pt-3'
}
}"
:refresher-enabled="true"
@pull="onPull"
@bottom="loadMore"
>
<template #item="{ value }">
<goods-item :value="value"></goods-item>
</template>
<template #bottom>
<view class="py-3">
<cl-loadmore
v-if="list.length > 0"
:loading="loading"
safe-area-bottom
></cl-loadmore>
</view>
</template>
</cl-list-view>
</cl-page>
</template>
<script lang="ts" setup>
import { useUi } from "@/uni_modules/cool-ui";
import { ref } from "vue";
import { usePager } from "@/cool";
import GoodsItem from "../components/goods-item.uvue";
import { t } from "@/locale";
const ui = useUi();
const listViewRef = ref<ClListViewComponentPublicInstance | null>(null);
let id = 0;
const { refresh, list, listView, loading, loadMore } = usePager((params, { render }) => {
// 模拟请求
setTimeout(() => {
render({
list: [
{
id: id++,
title: "春日樱花盛开时节,粉色花瓣如诗如画般飘洒",
image: "https://unix.cool-js.com/images/demo/1.jpg"
},
{
id: id++,
title: "夕阳西下的海滩边,金色阳光温柔地洒在波光粼粼的海面上,构成令人心旷神怡的日落美景",
image: "https://unix.cool-js.com/images/demo/2.jpg"
},
{
id: id++,
title: "寒冬腊月时分,洁白雪花纷纷扬扬地覆盖着整个世界,感受冬日的宁静与美好",
image: "https://unix.cool-js.com/images/demo/3.jpg"
},
{
id: id++,
title: "都市夜景霓虹闪烁,五彩斑斓光芒照亮城市营造梦幻般景象",
image: "https://unix.cool-js.com/images/demo/5.jpg"
},
{
id: id++,
title: "云雾缭绕的山间风光如诗如画让人心旷神怡,微风轻抚树梢带来阵阵清香,鸟儿在林间自由歌唱",
image: "https://unix.cool-js.com/images/demo/6.jpg"
},
{
id: id++,
title: "古老建筑与现代摩天大楼交相辉映,传统与现代完美融合创造独特城市景观",
image: "https://unix.cool-js.com/images/demo/7.jpg"
},
{
id: id++,
title: "广袤田野绿意盎然风光无限,金黄麦浪在微风中轻柔摇曳,农家炊烟袅袅升起",
image: "https://unix.cool-js.com/images/demo/8.jpg"
},
{
id: id++,
title: "璀璨星空下银河横跨天际,繁星闪烁神秘光芒营造浪漫夜空美景",
image: "https://unix.cool-js.com/images/demo/9.jpg"
},
{
id: id++,
title: "雄伟瀑布从高耸悬崖飞流直下激起千层浪花,彩虹在水雾中若隐若现如梦如幻",
image: "https://unix.cool-js.com/images/demo/10.jpg"
}
],
pagination: {
page: params["page"],
size: params["size"],
total: 100
}
});
ui.hideLoading();
}, 1000);
});
async function onPull() {
await refresh({ page: 1 });
listViewRef.value!.stopRefresh();
}
onReady(() => {
ui.showLoading(t("加载中"));
// 默认请求
refresh({});
});
</script>

View File

@@ -0,0 +1,68 @@
<template>
<cl-page>
<view class="page">
<view class="p-3 pb-0">
<demo-item>
<cl-text
>采用虚拟列表技术实现高性能渲染,支持海量数据无限滚动,当前演示数据规模:{{
data.length
}}条</cl-text
>
</demo-item>
</view>
<view class="list">
<cl-list-view
:data="data"
:pt="{
indexBar: {
className: '!fixed'
},
itemHover: {
className: 'bg-gray-200'
}
}"
>
</cl-list-view>
</view>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { request } from "@/cool";
import DemoItem from "../components/item.uvue";
import { useListView, useUi, type ClListViewItem } from "@/uni_modules/cool-ui";
import { ref } from "vue";
const ui = useUi();
const data = ref<ClListViewItem[]>([]);
onReady(() => {
ui.showLoading();
request({
url: "https://unix.cool-js.com/data/pca_flat.json"
})
.then((res) => {
data.value = useListView(res as UTSJSONObject[]);
})
.catch((err) => {
console.error(err);
})
.finally(() => {
ui.hideLoading();
});
});
</script>
<style lang="scss" scoped>
.page {
height: 100%;
.list {
flex: 1;
}
}
</style>

View File

@@ -0,0 +1,120 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<cl-list-item :label="t('用户名')">
<cl-text>神仙都没用</cl-text>
</cl-list-item>
</demo-item>
<demo-item :label="t('内容靠左')">
<cl-list-item
:label="t('QQ')"
justify="start"
:pt="{
label: {
className: '!w-10'
}
}"
>
<cl-text>615206459</cl-text>
</cl-list-item>
</demo-item>
<demo-item :label="t('带箭头')">
<cl-list-item label="年龄" arrow>
<cl-text>18</cl-text>
</cl-list-item>
</demo-item>
<demo-item :label="t('带图标')">
<cl-list-item :label="t('余额')" icon="wallet-line">
<cl-text>10,9000</cl-text>
</cl-list-item>
</demo-item>
<demo-item :label="t('带图片')">
<cl-list-item
arrow
:pt="{
image: {
width: 48,
height: 48
}
}"
:label="t('神仙都没用')"
image="https://unix.cool-js.com/images/demo/avatar.jpg"
>
</cl-list-item>
</demo-item>
<demo-item :label="t('折叠')">
<cl-list-item :label="t('点击展开')" collapse arrow>
<template #collapse>
<view class="bg-surface-100 dark:bg-surface-700 p-3 rounded-xl">
<cl-text
>云想衣裳花想容,春风拂槛露华浓。若非群玉山头见,会向瑶台月下逢。</cl-text
>
</view>
</template>
</cl-list-item>
</demo-item>
<demo-item :label="t('可滑动')">
<cl-list-item :label="t('左滑编辑')" swipeable>
<template #swipe-right>
<view
class="bg-green-500 w-20 h-full flex flex-row items-center justify-center"
>
<text class="text-white text-md">{{ t("编辑") }}</text>
</view>
</template>
</cl-list-item>
<cl-list-item ref="listItemRef" :label="t('右滑删除')" swipeable>
<template #swipe-left>
<view
class="bg-red-500 w-20 h-full flex flex-row items-center justify-center"
@tap="onDelete"
>
<text class="text-white text-md">{{ t("删除") }}</text>
</view>
</template>
</cl-list-item>
</demo-item>
<demo-item :label="t('禁用')">
<cl-list-item :label="t('账号')" disabled>
<cl-text>1234567890</cl-text>
</cl-list-item>
</demo-item>
<demo-item :label="t('列表')">
<cl-list border>
<cl-list-item :label="t('我的订单')" hoverable> </cl-list-item>
<cl-list-item :label="t('我的收藏')" hoverable> </cl-list-item>
<cl-list-item :label="t('我的钱包')" hoverable> </cl-list-item>
</cl-list>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { t } from "@/locale";
import DemoItem from "../components/item.uvue";
import { useUi } from "@/uni_modules/cool-ui";
import { ref } from "vue";
const ui = useUi();
const listItemRef = ref<ClListItemComponentPublicInstance | null>(null);
function onDelete() {
ui.showToast({
message: "删除成功"
});
listItemRef.value!.resetSwipe();
}
</script>

View File

@@ -0,0 +1,70 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('横向滚动')">
<cl-marquee
:list="list"
direction="horizontal"
:item-height="200"
:item-width="360"
:pt="{
className: 'h-[200rpx] rounded-xl'
}"
></cl-marquee>
</demo-item>
<demo-item :label="t('纵向滚动')">
<cl-marquee
ref="marqueeRef"
:list="list"
direction="vertical"
:item-height="260"
:duration="isSpeed ? 2000 : 5000"
:pt="{
className: 'h-[500rpx] rounded-xl'
}"
></cl-marquee>
<cl-list
border
:pt="{
className: 'mt-5'
}"
>
<cl-list-item :label="t('快一点')">
<cl-switch v-model="isSpeed"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('暂停')">
<cl-switch v-model="isPause" @change="onPauseChange"></cl-switch>
</cl-list-item>
</cl-list>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import DemoItem from "../components/item.uvue";
import { t } from "@/locale";
import { ref } from "vue";
const marqueeRef = ref<ClMarqueeComponentPublicInstance | null>(null);
const list = ref<string[]>([
"https://unix.cool-js.com/images/demo/bg1.png",
"https://unix.cool-js.com/images/demo/bg2.png",
"https://unix.cool-js.com/images/demo/bg3.png"
]);
const isSpeed = ref(false);
const isPause = ref(false);
function onPauseChange(value: boolean) {
if (value) {
marqueeRef.value!.pause();
} else {
marqueeRef.value!.play();
}
}
</script>

View File

@@ -0,0 +1,70 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<cl-pagination v-model="page1" :total="24"> </cl-pagination>
</demo-item>
<demo-item :label="t('多页数')">
<cl-pagination v-model="page2" :total="500"> </cl-pagination>
</demo-item>
<demo-item :label="t('自定义样式')">
<cl-pagination
v-model="page3"
:total="100"
:pt="{
item: {
className: '!rounded-none !mx-[2rpx]'
}
}"
>
</cl-pagination>
</demo-item>
<demo-item :label="t('自定义文本')">
<cl-pagination
v-model="page4"
:total="24"
:pt="{
prev: {
className: '!w-auto px-3'
},
next: {
className: '!w-auto px-3'
}
}"
>
<template #prev>
<cl-text
:pt="{
className: 'text-sm'
}"
>{{ t("上一页") }}</cl-text
>
</template>
<template #next>
<cl-text
:pt="{
className: 'text-sm'
}"
>{{ t("下一页") }}</cl-text
>
</template>
</cl-pagination>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { t } from "@/locale";
import DemoItem from "../components/item.uvue";
const page1 = ref(1);
const page2 = ref(13);
const page3 = ref(1);
const page4 = ref(1);
</script>

Some files were not shown because too many files have changed in this diff Show More