init
This commit is contained in:
364
common/IndexedDBHelper.js
Normal file
364
common/IndexedDBHelper.js
Normal file
@@ -0,0 +1,364 @@
|
||||
class IndexedDBHelper {
|
||||
constructor(dbName, version = 1) {
|
||||
this.dbName = dbName;
|
||||
this.version = version;
|
||||
this.db = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化数据库(打开/创建)
|
||||
* @param {Array} stores [{ name: "storeName", keyPath: "id", indexes: [{ name: "indexName", key: "keyPath", unique: false }] }]
|
||||
* @returns {Promise}
|
||||
*/
|
||||
openDB(stores = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(this.dbName, this.version);
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
this.db = event.target.result;
|
||||
stores.forEach(store => {
|
||||
if (!this.db.objectStoreNames.contains(store.name)) {
|
||||
const objectStore = this.db.createObjectStore(store.name, {
|
||||
keyPath: store.keyPath,
|
||||
autoIncrement: store.autoIncrement || false
|
||||
});
|
||||
if (store.indexes) {
|
||||
store.indexes.forEach(index => {
|
||||
objectStore.createIndex(index.name, index.key, {
|
||||
unique: index.unique
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
this.db = event.target.result;
|
||||
console.log("✅ IndexedDB 连接成功");
|
||||
resolve(this.db);
|
||||
};
|
||||
|
||||
request.onerror = (event) => {
|
||||
reject(`IndexedDB Error: ${event.target.error}`);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// 通用查询方法,按指定字段查询
|
||||
async queryByField(storeName, fieldName, value) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
if (!this.db) {
|
||||
await this.openDB();
|
||||
}
|
||||
const transaction = this.db.transaction(storeName, 'readonly');
|
||||
const store = transaction.objectStore(storeName);
|
||||
|
||||
if (!store.indexNames.contains(fieldName)) {
|
||||
return reject(`索引 ${fieldName} 不存在`);
|
||||
}
|
||||
|
||||
const index = store.index(fieldName);
|
||||
const request = index.getAll(value);
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
resolve(event.target.result);
|
||||
};
|
||||
|
||||
request.onerror = (event) => {
|
||||
reject('查询失败: ' + event.target.error);
|
||||
};
|
||||
} catch (error) {
|
||||
reject('查询错误: ' + error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加数据(支持单条或批量)
|
||||
* @param {string} storeName - 存储空间名称
|
||||
* @param {Object|Array} data - 要添加的数据(单条对象或数组)
|
||||
* @returns {Promise<number|Array<number>>} - 返回添加数据的ID(单条返回数字,批量返回数组)
|
||||
*/
|
||||
add(storeName, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([storeName], "readwrite");
|
||||
const store = transaction.objectStore(storeName);
|
||||
|
||||
// 统一处理为数组格式
|
||||
const items = Array.isArray(data) ? data : [data];
|
||||
const results = [];
|
||||
|
||||
// 监听每个添加操作的成功事件
|
||||
items.forEach((item, index) => {
|
||||
const request = store.add(item);
|
||||
request.onsuccess = (event) => {
|
||||
results[index] = event.target.result; // 保存生成的ID
|
||||
};
|
||||
request.onerror = (event) => {
|
||||
transaction.abort(); // 遇到错误时中止事务
|
||||
reject(`第 ${index + 1} 条数据添加失败: ${event.target.error}`);
|
||||
};
|
||||
});
|
||||
|
||||
// 监听事务完成事件
|
||||
transaction.oncomplete = () => {
|
||||
// 单条数据返回单个ID,批量返回数组
|
||||
resolve(items.length === 1 ? results[0] : results);
|
||||
};
|
||||
|
||||
// 统一错误处理
|
||||
transaction.onerror = (event) => {
|
||||
reject(`添加失败: ${event.target.error}`);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取数据(根据主键)
|
||||
* @param {string} storeName
|
||||
* @param {any} key
|
||||
* @returns {Promise}
|
||||
*/
|
||||
get(storeName, key) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([storeName], "readonly");
|
||||
const store = transaction.objectStore(storeName);
|
||||
const request = store.get(key);
|
||||
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = (event) => reject(`Get Error: ${event.target.error}`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取所有数据(兼容X5内核方案)
|
||||
* @param {string} storeName
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getAll(storeName) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([storeName], "readonly");
|
||||
const store = transaction.objectStore(storeName);
|
||||
|
||||
// 兼容性检测:优先尝试原生getAll方法
|
||||
if (typeof store.getAll === 'function') {
|
||||
const request = store.getAll();
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = (e) => reject(`GetAll Error: ${e.target.error}`);
|
||||
}
|
||||
// 降级方案:使用游标手动遍历
|
||||
else {
|
||||
const results = [];
|
||||
const request = store.openCursor();
|
||||
|
||||
request.onsuccess = (e) => {
|
||||
const cursor = e.target.result;
|
||||
if (cursor) {
|
||||
results.push(cursor.value);
|
||||
cursor.continue();
|
||||
} else {
|
||||
resolve(results);
|
||||
}
|
||||
};
|
||||
|
||||
request.onerror = (e) => reject(`Cursor Error: ${e.target.error}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取表的总记录数
|
||||
* @param {string} storeName - 表名(Object Store 名称)
|
||||
* @returns {Promise<number>} - 记录总数
|
||||
*/
|
||||
async getRecordCount(storeName) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([storeName], "readonly");
|
||||
const store = transaction.objectStore(storeName);
|
||||
const request = store.count();
|
||||
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = (event) => reject(`❌ Count Error: ${event.target.error}`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新数据(支持指定 key 或自动使用 keyPath)
|
||||
* @param {string} storeName 存储对象名
|
||||
* @param {Object} data 待更新数据
|
||||
* @param {IDBValidKey|IDBKeyRange} [key] 可选参数,指定更新的 key
|
||||
* @returns {Promise<string>} 更新结果
|
||||
*/
|
||||
update(storeName, data, key) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([storeName], "readwrite");
|
||||
const store = transaction.objectStore(storeName);
|
||||
const keyPath = store.keyPath;
|
||||
|
||||
// 有传入 key:直接使用 key 更新
|
||||
if (key !== undefined) {
|
||||
const request = store.put(data, key);
|
||||
request.onsuccess = () => resolve("数据更新成功(指定 key)");
|
||||
request.onerror = (event) => reject(`更新失败: ${event.target.error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 无传入 key:依赖 keyPath 更新
|
||||
if (!keyPath) {
|
||||
reject("当前 store 未设置 keyPath,必须传入 key 参数");
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查数据是否包含 keyPath 属性
|
||||
let missingKeys = [];
|
||||
if (Array.isArray(keyPath)) {
|
||||
missingKeys = keyPath.filter(k => !data.hasOwnProperty(k));
|
||||
} else if (typeof keyPath === 'string') {
|
||||
if (!data.hasOwnProperty(keyPath)) {
|
||||
missingKeys.push(keyPath);
|
||||
}
|
||||
}
|
||||
|
||||
if (missingKeys.length > 0) {
|
||||
reject(`数据缺少必要的 keyPath 属性: ${missingKeys.join(', ')}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 默认使用 keyPath 更新
|
||||
const request = store.put(data);
|
||||
request.onsuccess = () => resolve("数据更新成功(默认 keyPath)");
|
||||
request.onerror = (event) => reject(`更新失败: ${event.target.error}`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除数据(根据主键)
|
||||
* @param {string} storeName
|
||||
* @param {any} key
|
||||
* @returns {Promise}
|
||||
*/
|
||||
delete(storeName, key) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([storeName], "readwrite");
|
||||
const store = transaction.objectStore(storeName);
|
||||
const request = store.delete(key);
|
||||
|
||||
request.onsuccess = () => resolve("Data deleted successfully");
|
||||
request.onerror = (event) => reject(`Delete Error: ${event.target.error}`);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 根据条件删除所有匹配的数据
|
||||
* @param {string} storeName - 数据仓库名
|
||||
* @param {function} conditionFn - 判断是否删除 (record) => boolean
|
||||
* @returns {Promise}
|
||||
*/
|
||||
deleteByCondition(storeName, conditionFn) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.db) {
|
||||
reject('Database not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
const transaction = this.db.transaction([storeName], 'readwrite');
|
||||
const store = transaction.objectStore(storeName);
|
||||
const request = store.openCursor();
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
const cursor = event.target.result;
|
||||
if (cursor && cursor.value) {
|
||||
try {
|
||||
// console.log(cursor.value)
|
||||
const shouldDelete = conditionFn(cursor.value);
|
||||
if (shouldDelete) {
|
||||
cursor.delete();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Condition function error:', err);
|
||||
}
|
||||
cursor.continue();
|
||||
} else {
|
||||
resolve('All matching records deleted successfully');
|
||||
}
|
||||
};
|
||||
|
||||
request.onerror = (event) => {
|
||||
reject(`Delete by condition failed: ${event.target.error}`);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过索引查询数据
|
||||
* @param {string} storeName
|
||||
* @param {string} indexName
|
||||
* @param {any} value
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getByIndex(storeName, indexName, value) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([storeName], "readonly");
|
||||
const store = transaction.objectStore(storeName);
|
||||
const index = store.index(indexName);
|
||||
const request = index.get(value);
|
||||
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = (event) => reject(`Get By Index Error: ${event.target.error}`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空表
|
||||
* @param {string} storeName
|
||||
* @returns {Promise}
|
||||
*/
|
||||
clearStore(storeName) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([storeName], "readwrite");
|
||||
const store = transaction.objectStore(storeName);
|
||||
const request = store.clear();
|
||||
|
||||
request.onsuccess = () => resolve("Store cleared successfully");
|
||||
request.onerror = (event) => reject(`Clear Store Error: ${event.target.error}`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除数据库
|
||||
* @returns {Promise}
|
||||
*/
|
||||
deleteDB(dbNamed = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.deleteDatabase(dbNamed || this.dbName);
|
||||
|
||||
request.onsuccess = () => resolve("Database deleted successfully");
|
||||
request.onerror = (event) => reject(`Delete DB Error: ${event.target.error}`);
|
||||
});
|
||||
}
|
||||
|
||||
async deleteOldestRecord(storeName) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([storeName], "readwrite");
|
||||
const store = transaction.objectStore(storeName);
|
||||
const request = store.openCursor(); // 🔹 获取最早的记录(按主键 ID 排序)
|
||||
|
||||
request.onsuccess = function(event) {
|
||||
const cursor = event.target.result;
|
||||
if (cursor) {
|
||||
console.log(`🗑️ 删除最早的记录 ID: ${cursor.key}`);
|
||||
store.delete(cursor.key); // 🔥 删除最小 ID(最早记录)
|
||||
resolve();
|
||||
} else {
|
||||
resolve(); // 没有记录时跳过
|
||||
}
|
||||
};
|
||||
|
||||
request.onerror = (event) => reject(`❌ Cursor Error: ${event.target.error}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default IndexedDBHelper
|
||||
256
common/UniStorageHelper.js
Normal file
256
common/UniStorageHelper.js
Normal file
@@ -0,0 +1,256 @@
|
||||
// uni-storage-helper.js
|
||||
class UniStorageHelper {
|
||||
constructor(dbName, options = {}) {
|
||||
this.dbName = dbName;
|
||||
this.storesMeta = {};
|
||||
this.options = {
|
||||
maxEntries: 500, // 单个存储空间最大条目数
|
||||
maxSizeMB: 1, // 单条数据最大限制(微信小程序限制)
|
||||
autoPurge: true, // 是否自动清理旧数据
|
||||
purgeBatch: 10, // 自动清理批次数量
|
||||
debug: false, // 调试模式
|
||||
...options
|
||||
};
|
||||
}
|
||||
|
||||
/*==================
|
||||
核心方法
|
||||
==================*/
|
||||
|
||||
/**
|
||||
* 初始化存储空间
|
||||
* @param {Array} stores - 存储空间配置
|
||||
*/
|
||||
async openDB(stores = []) {
|
||||
stores.forEach(store => {
|
||||
const storeKey = this._getStoreKey(store.name);
|
||||
if (!this._storageHas(storeKey)) {
|
||||
this._storageSet(storeKey, []);
|
||||
}
|
||||
|
||||
this.storesMeta[store.name] = {
|
||||
keyPath: store.keyPath,
|
||||
autoIncrement: !!store.autoIncrement,
|
||||
indexes: store.indexes || []
|
||||
};
|
||||
|
||||
if (store.autoIncrement) {
|
||||
const counterKey = this._getCounterKey(store.name);
|
||||
if (!this._storageHas(counterKey)) {
|
||||
this._storageSet(counterKey, 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this._log('数据库初始化完成');
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加数据(自动处理容量限制)
|
||||
*/
|
||||
async add(storeName, data) {
|
||||
try {
|
||||
const storeKey = this._getStoreKey(storeName);
|
||||
let storeData = this._storageGet(storeKey) || [];
|
||||
const meta = this.storesMeta[storeName];
|
||||
const items = Array.isArray(data) ? data : [data];
|
||||
|
||||
// 容量预检
|
||||
await this._checkCapacity(storeName, items);
|
||||
|
||||
// 处理自增ID
|
||||
if (meta?.autoIncrement) {
|
||||
const counterKey = this._getCounterKey(storeName);
|
||||
let nextId = this._storageGet(counterKey) || 1;
|
||||
items.forEach(item => {
|
||||
item[meta.keyPath] = nextId++;
|
||||
this._createIndexes(meta.indexes, item);
|
||||
});
|
||||
this._storageSet(counterKey, nextId);
|
||||
}
|
||||
|
||||
// 保存数据
|
||||
storeData = [...storeData, ...items];
|
||||
this._storageSet(storeKey, storeData);
|
||||
|
||||
this._log(`成功添加${items.length}条数据到${storeName}`);
|
||||
|
||||
return meta?.autoIncrement ?
|
||||
Array.isArray(data) ?
|
||||
items.map(i => i[meta.keyPath]) :
|
||||
items[0][meta.keyPath] :
|
||||
undefined;
|
||||
|
||||
} catch (error) {
|
||||
if (error.message.includes('exceed')) {
|
||||
this._log('触发自动清理...');
|
||||
await this._purgeData(storeName, this.options.purgeBatch);
|
||||
return this.add(storeName, data);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/*==================
|
||||
查询方法
|
||||
==================*/
|
||||
|
||||
async get(storeName, key) {
|
||||
const storeData = this._storageGet(this._getStoreKey(storeName)) || [];
|
||||
const keyPath = this.storesMeta[storeName]?.keyPath;
|
||||
return storeData.find(item => item[keyPath] === key);
|
||||
}
|
||||
|
||||
async getAll(storeName) {
|
||||
return this._storageGet(this._getStoreKey(storeName)) || [];
|
||||
}
|
||||
|
||||
async queryByField(storeName, fieldName, value) {
|
||||
const storeData = this._storageGet(this._getStoreKey(storeName)) || [];
|
||||
return storeData.filter(item => item[fieldName] === value);
|
||||
}
|
||||
|
||||
/*==================
|
||||
更新/删除方法
|
||||
==================*/
|
||||
|
||||
async update(storeName, data, key) {
|
||||
const storeKey = this._getStoreKey(storeName);
|
||||
const storeData = this._storageGet(storeKey) || [];
|
||||
const meta = this.storesMeta[storeName];
|
||||
const keyPath = meta?.keyPath;
|
||||
const targetKey = key ?? data[keyPath];
|
||||
|
||||
const index = storeData.findIndex(item => item[keyPath] === targetKey);
|
||||
if (index === -1) throw new Error('未找到对应记录');
|
||||
|
||||
// 合并数据并重建索引
|
||||
const newItem = {
|
||||
...storeData[index],
|
||||
...data
|
||||
};
|
||||
this._createIndexes(meta.indexes, newItem);
|
||||
|
||||
storeData[index] = newItem;
|
||||
this._storageSet(storeKey, storeData);
|
||||
|
||||
return "更新成功";
|
||||
}
|
||||
|
||||
async delete(storeName, key) {
|
||||
const storeKey = this._getStoreKey(storeName);
|
||||
const storeData = this._storageGet(storeKey) || [];
|
||||
const keyPath = this.storesMeta[storeName]?.keyPath;
|
||||
const newData = storeData.filter(item => item[keyPath] !== key);
|
||||
this._storageSet(storeKey, newData);
|
||||
return `删除${storeData.length - newData.length}条记录`;
|
||||
}
|
||||
|
||||
/*==================
|
||||
存储管理
|
||||
==================*/
|
||||
|
||||
async clearStore(storeName) {
|
||||
this._storageSet(this._getStoreKey(storeName), []);
|
||||
return "存储空间已清空";
|
||||
}
|
||||
|
||||
async deleteDB() {
|
||||
Object.keys(this.storesMeta).forEach(storeName => {
|
||||
uni.removeStorageSync(this._getStoreKey(storeName));
|
||||
uni.removeStorageSync(this._getCounterKey(storeName));
|
||||
});
|
||||
return "数据库已删除";
|
||||
}
|
||||
|
||||
/*==================
|
||||
私有方法
|
||||
==================*/
|
||||
|
||||
_getStoreKey(storeName) {
|
||||
return `${this.dbName}_${storeName}`;
|
||||
}
|
||||
|
||||
_getCounterKey(storeName) {
|
||||
return `${this.dbName}_${storeName}_counter`;
|
||||
}
|
||||
|
||||
_createIndexes(indexes, item) {
|
||||
indexes.forEach(index => {
|
||||
item[index.name] = item[index.key];
|
||||
});
|
||||
}
|
||||
|
||||
async _checkCapacity(storeName, newItems) {
|
||||
const storeKey = this._getStoreKey(storeName);
|
||||
const currentData = this._storageGet(storeKey) || [];
|
||||
|
||||
// 检查条目数限制
|
||||
if (currentData.length + newItems.length > this.options.maxEntries) {
|
||||
await this._purgeData(storeName, newItems.length);
|
||||
}
|
||||
|
||||
// 检查单条数据大小
|
||||
newItems.forEach(item => {
|
||||
const sizeMB = this._getItemSizeMB(item);
|
||||
if (sizeMB > this.options.maxSizeMB) {
|
||||
throw new Error(`单条数据大小超出${this.options.maxSizeMB}MB限制`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_getItemSizeMB(item) {
|
||||
try {
|
||||
// 精确计算(支持Blob的环境)
|
||||
return new Blob([JSON.stringify(item)]).size / 1024 / 1024;
|
||||
} catch {
|
||||
// 兼容方案
|
||||
return encodeURIComponent(JSON.stringify(item)).length * 2 / 1024 / 1024;
|
||||
}
|
||||
}
|
||||
|
||||
async _purgeData(storeName, count) {
|
||||
const storeKey = this._getStoreKey(storeName);
|
||||
const currentData = this._storageGet(storeKey) || [];
|
||||
const newData = currentData.slice(count);
|
||||
this._storageSet(storeKey, newData);
|
||||
this._log(`自动清理${count}条旧数据`);
|
||||
}
|
||||
|
||||
/*==================
|
||||
存储适配器
|
||||
==================*/
|
||||
|
||||
_storageHas(key) {
|
||||
return !!uni.getStorageSync(key);
|
||||
}
|
||||
|
||||
_storageGet(key) {
|
||||
try {
|
||||
return uni.getStorageSync(key);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
_storageSet(key, value) {
|
||||
try {
|
||||
uni.setStorageSync(key, value);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error.errMsg?.includes('exceed')) {
|
||||
throw new Error('STORAGE_QUOTA_EXCEEDED');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
_log(...args) {
|
||||
if (this.options.debug) {
|
||||
console.log(`[StorageHelper]`, ...args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default UniStorageHelper;
|
||||
193
common/animation.css
Normal file
193
common/animation.css
Normal file
@@ -0,0 +1,193 @@
|
||||
/*base code*/
|
||||
.animated {
|
||||
-webkit-animation-duration: 1s;
|
||||
animation-duration: 1s;
|
||||
-webkit-animation-fill-mode: both;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
.animated.infinite {
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
.animated.hinge {
|
||||
-webkit-animation-duration: 2s;
|
||||
animation-duration: 2s;
|
||||
}
|
||||
|
||||
/*the animation definition*/
|
||||
@-webkit-keyframes tada {
|
||||
0% {
|
||||
-webkit-transform: scale3d(1, 1, 1);
|
||||
transform: scale3d(1, 1, 1)
|
||||
}
|
||||
|
||||
10%,
|
||||
20% {
|
||||
-webkit-transform: scale3d(.9, .9, .9) rotate3d(0, 0, 1, -3deg);
|
||||
transform: scale3d(.9, .9, .9) rotate3d(0, 0, 1, -3deg)
|
||||
}
|
||||
|
||||
30%,
|
||||
50%,
|
||||
70%,
|
||||
90% {
|
||||
-webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg);
|
||||
transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg)
|
||||
}
|
||||
|
||||
40%,
|
||||
60%,
|
||||
80% {
|
||||
-webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg);
|
||||
transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg)
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: scale3d(1, 1, 1);
|
||||
transform: scale3d(1, 1, 1)
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes tada {
|
||||
0% {
|
||||
-webkit-transform: scale3d(1, 1, 1);
|
||||
-ms-transform: scale3d(1, 1, 1);
|
||||
transform: scale3d(1, 1, 1)
|
||||
}
|
||||
|
||||
10%,
|
||||
20% {
|
||||
-webkit-transform: scale3d(.9, .9, .9) rotate3d(0, 0, 1, -3deg);
|
||||
-ms-transform: scale3d(.9, .9, .9) rotate3d(0, 0, 1, -3deg);
|
||||
transform: scale3d(.9, .9, .9) rotate3d(0, 0, 1, -3deg)
|
||||
}
|
||||
|
||||
30%,
|
||||
50%,
|
||||
70%,
|
||||
90% {
|
||||
-webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg);
|
||||
-ms-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg);
|
||||
transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg)
|
||||
}
|
||||
|
||||
40%,
|
||||
60%,
|
||||
80% {
|
||||
-webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg);
|
||||
-ms-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg);
|
||||
transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg)
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: scale3d(1, 1, 1);
|
||||
-ms-transform: scale3d(1, 1, 1);
|
||||
transform: scale3d(1, 1, 1)
|
||||
}
|
||||
}
|
||||
|
||||
.tada {
|
||||
-webkit-animation-name: tada;
|
||||
animation-name: tada
|
||||
}
|
||||
|
||||
|
||||
.btn-tada:active {
|
||||
-webkit-animation-name: tada;
|
||||
animation-name: tada
|
||||
}
|
||||
|
||||
/*the animation definition*/
|
||||
@-webkit-keyframes rubberBand {
|
||||
0% {
|
||||
-webkit-transform: scale3d(1, 1, 1);
|
||||
transform: scale3d(1, 1, 1)
|
||||
}
|
||||
|
||||
30% {
|
||||
-webkit-transform: scale3d(1.25, .75, 1);
|
||||
transform: scale3d(1.25, .75, 1)
|
||||
}
|
||||
|
||||
40% {
|
||||
-webkit-transform: scale3d(0.75, 1.25, 1);
|
||||
transform: scale3d(0.75, 1.25, 1)
|
||||
}
|
||||
|
||||
50% {
|
||||
-webkit-transform: scale3d(1.15, .85, 1);
|
||||
transform: scale3d(1.15, .85, 1)
|
||||
}
|
||||
|
||||
65% {
|
||||
-webkit-transform: scale3d(.95, 1.05, 1);
|
||||
transform: scale3d(.95, 1.05, 1)
|
||||
}
|
||||
|
||||
75% {
|
||||
-webkit-transform: scale3d(1.05, .95, 1);
|
||||
transform: scale3d(1.05, .95, 1)
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: scale3d(1, 1, 1);
|
||||
transform: scale3d(1, 1, 1)
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rubberBand {
|
||||
0% {
|
||||
-webkit-transform: scale3d(1, 1, 1);
|
||||
-ms-transform: scale3d(1, 1, 1);
|
||||
transform: scale3d(1, 1, 1)
|
||||
}
|
||||
|
||||
30% {
|
||||
-webkit-transform: scale3d(1.25, .75, 1);
|
||||
-ms-transform: scale3d(1.25, .75, 1);
|
||||
transform: scale3d(1.25, .75, 1)
|
||||
}
|
||||
|
||||
40% {
|
||||
-webkit-transform: scale3d(0.75, 1.25, 1);
|
||||
-ms-transform: scale3d(0.75, 1.25, 1);
|
||||
transform: scale3d(0.75, 1.25, 1)
|
||||
}
|
||||
|
||||
50% {
|
||||
-webkit-transform: scale3d(1.15, .85, 1);
|
||||
-ms-transform: scale3d(1.15, .85, 1);
|
||||
transform: scale3d(1.15, .85, 1)
|
||||
}
|
||||
|
||||
65% {
|
||||
-webkit-transform: scale3d(.95, 1.05, 1);
|
||||
-ms-transform: scale3d(.95, 1.05, 1);
|
||||
transform: scale3d(.95, 1.05, 1)
|
||||
}
|
||||
|
||||
75% {
|
||||
-webkit-transform: scale3d(1.05, .95, 1);
|
||||
-ms-transform: scale3d(1.05, .95, 1);
|
||||
transform: scale3d(1.05, .95, 1)
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: scale3d(1, 1, 1);
|
||||
-ms-transform: scale3d(1, 1, 1);
|
||||
transform: scale3d(1, 1, 1)
|
||||
}
|
||||
}
|
||||
|
||||
.rubberBand {
|
||||
-webkit-animation-name: rubberBand;
|
||||
animation-name: rubberBand
|
||||
}
|
||||
|
||||
|
||||
.btn-rubberBand:active {
|
||||
-webkit-animation-name: tada;
|
||||
animation-name: tada
|
||||
}
|
||||
467
common/common.css
Normal file
467
common/common.css
Normal file
@@ -0,0 +1,467 @@
|
||||
/* 公共样式表 */
|
||||
page {
|
||||
min-height: calc(100vh - var(--window-top) - var(--status-bar-height) - var(--window-bottom));
|
||||
font-size: 28rpx;
|
||||
background-color: #FFFFFF;
|
||||
color: #333333;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 禁止页面回弹 */
|
||||
/* html,
|
||||
body,
|
||||
page {
|
||||
overscroll-behavior: none;
|
||||
overflow: hidden;
|
||||
} */
|
||||
|
||||
image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.page-body {
|
||||
height: calc(100vh - var(--window-top) - var(--status-bar-height) - var(--window-bottom));
|
||||
/* width: 100%; */
|
||||
/* height: 100%; */
|
||||
}
|
||||
|
||||
body,
|
||||
html {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* 布局调整 */
|
||||
|
||||
/* 点击动效 */
|
||||
/* 缩小 */
|
||||
.button-click {
|
||||
transition: transform 0.1s ease;
|
||||
}
|
||||
|
||||
.button-click:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* 背景变色 */
|
||||
.btn-light {
|
||||
color: white;
|
||||
border-radius: 16rpx;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.btn-light:active {
|
||||
background-color: rgba(189, 197, 254, 0.15);
|
||||
}
|
||||
|
||||
|
||||
.btn-incline {
|
||||
transition: transform 0.2s ease;
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.btn-incline:active {
|
||||
transform: perspective(600px) rotateY(6deg) rotateX(3deg);
|
||||
}
|
||||
|
||||
.btn-feel {
|
||||
transition: transform 0.2s ease;
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.btn-feel:active {
|
||||
transform: perspective(600px) rotateX(6deg) scale(0.98);
|
||||
}
|
||||
|
||||
.press-button {
|
||||
padding: 10px 20px;
|
||||
background: #3A4750;
|
||||
/* 深灰蓝 */
|
||||
color: #ffffff;
|
||||
font-size: 16px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.1s ease, box-shadow 0.1s ease;
|
||||
/* box-shadow: 0 4px 0 #2C3E50; */
|
||||
}
|
||||
|
||||
.press-button:active {
|
||||
transform: scale(0.95) translateY(2px);
|
||||
/* box-shadow: 0 2px 0 #1C2833; */
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
.btn-shaky:active {
|
||||
animation: shakeScale 0.6s;
|
||||
}
|
||||
|
||||
@keyframes shakeScale {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
10% {
|
||||
transform: scale(0.9) rotate(-3deg);
|
||||
}
|
||||
|
||||
20% {
|
||||
transform: scale(1.05) rotate(3deg);
|
||||
}
|
||||
|
||||
30% {
|
||||
transform: scale(0.95) rotate(-3deg);
|
||||
}
|
||||
|
||||
40% {
|
||||
transform: scale(1.02) rotate(3deg);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(0.98) rotate(-2deg);
|
||||
}
|
||||
|
||||
60% {
|
||||
transform: scale(1.01) rotate(2deg);
|
||||
}
|
||||
|
||||
70% {
|
||||
transform: scale(0.99) rotate(-1deg);
|
||||
}
|
||||
|
||||
80% {
|
||||
transform: scale(1.005) rotate(1deg);
|
||||
}
|
||||
|
||||
90% {
|
||||
transform: scale(1) rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1) rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* 控制hover */
|
||||
.opctiy_8 {
|
||||
opacity: 0.8 !important;
|
||||
}
|
||||
|
||||
.opctiy_7 {
|
||||
opacity: 0.7 !important;
|
||||
}
|
||||
|
||||
.opctiy_6 {
|
||||
opacity: 0.6 !important;
|
||||
}
|
||||
|
||||
.opctiy_5 {
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
|
||||
.opctiy_4 {
|
||||
opacity: 0.4 !important;
|
||||
}
|
||||
|
||||
.opctiy_3 {
|
||||
opacity: 0.3 !important;
|
||||
}
|
||||
|
||||
.opctiy_2 {
|
||||
opacity: 0.2 !important;
|
||||
}
|
||||
|
||||
.opctiy_1 {
|
||||
opacity: 0.1 !important;
|
||||
}
|
||||
|
||||
/* 控制文字大小 */
|
||||
.fs_10 {
|
||||
font-size: 20rpx !important;
|
||||
}
|
||||
|
||||
.fs_12 {
|
||||
font-size: 24rpx !important;
|
||||
}
|
||||
|
||||
.fs_14 {
|
||||
font-size: 28rpx !important;
|
||||
}
|
||||
|
||||
.fs_16 {
|
||||
font-size: 32rpx !important;
|
||||
}
|
||||
|
||||
.fs_18 {
|
||||
font-size: 36rpx !important;
|
||||
}
|
||||
|
||||
.fs_20 {
|
||||
font-size: 40rpx !important;
|
||||
}
|
||||
|
||||
.fs_22 {
|
||||
font-size: 44rpx !important;
|
||||
}
|
||||
|
||||
.fs_24 {
|
||||
font-size: 48rpx !important;
|
||||
}
|
||||
|
||||
.fs_26 {
|
||||
font-size: 52rpx !important;
|
||||
}
|
||||
|
||||
.fs_28 {
|
||||
font-size: 56rpx !important;
|
||||
}
|
||||
|
||||
.fs_30 {
|
||||
font-size: 60rpx !important;
|
||||
}
|
||||
|
||||
.fs_32 {
|
||||
font-size: 64rpx !important;
|
||||
}
|
||||
|
||||
/* 控制字体粗细 */
|
||||
.fw_blod {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 控制字体颜色 */
|
||||
.color_D16B3F {
|
||||
color: #D16B3F !important;
|
||||
}
|
||||
|
||||
.color_C7331D {
|
||||
color: #C7331D !important;
|
||||
}
|
||||
|
||||
.color_666666 {
|
||||
color: #666666 !important;
|
||||
}
|
||||
|
||||
.color_F8A52F {
|
||||
color: #F8A52F !important;
|
||||
}
|
||||
|
||||
.color_999999 {
|
||||
color: #999999 !important;
|
||||
}
|
||||
|
||||
.color_C7331D {
|
||||
color: #C7331D !important;
|
||||
}
|
||||
|
||||
.color_333333 {
|
||||
color: #333333 !important;
|
||||
}
|
||||
|
||||
.color_FFFFFF {
|
||||
color: #FFFFFF !important;
|
||||
}
|
||||
|
||||
.color_E7612E {
|
||||
color: #E7612E !important;
|
||||
}
|
||||
|
||||
.color_EF4B37 {
|
||||
color: #EF4B37 !important;
|
||||
}
|
||||
|
||||
.color_5F5F5F {
|
||||
color: #5F5F5F !important;
|
||||
}
|
||||
|
||||
.color_FB7307 {
|
||||
color: #FB7307 !important;
|
||||
}
|
||||
|
||||
.color_256BFA {
|
||||
color: #256BFA !important;
|
||||
}
|
||||
|
||||
.color_4E8ADE {
|
||||
color: #4E8ADE !important;
|
||||
}
|
||||
|
||||
.color_D9D9D9 {
|
||||
color: #D9D9D9 !important;
|
||||
}
|
||||
|
||||
/* 控制左右距离 */
|
||||
.mar_le30 {
|
||||
margin-left: 60rpx !important;
|
||||
}
|
||||
|
||||
.mar_le25 {
|
||||
margin-left: 50rpx !important;
|
||||
}
|
||||
|
||||
.mar_le20 {
|
||||
margin-left: 40rpx !important;
|
||||
}
|
||||
|
||||
.mar_le15 {
|
||||
margin-left: 30rpx !important;
|
||||
}
|
||||
|
||||
.mar_le10 {
|
||||
margin-left: 20rpx !important;
|
||||
}
|
||||
|
||||
.mar_le5 {
|
||||
margin-left: 10rpx !important;
|
||||
}
|
||||
|
||||
.mar_ri5 {
|
||||
margin-right: 10rpx !important;
|
||||
}
|
||||
|
||||
.mar_ri10 {
|
||||
margin-right: 20rpx !important;
|
||||
}
|
||||
|
||||
.mar_ri15 {
|
||||
margin-right: 30rpx !important;
|
||||
}
|
||||
|
||||
.mar_ri20 {
|
||||
margin-right: 40rpx !important;
|
||||
}
|
||||
|
||||
.mar_ri25 {
|
||||
margin-right: 50rpx !important;
|
||||
}
|
||||
|
||||
.mar_top0 {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.mar_top5 {
|
||||
margin-top: 10rpx !important;
|
||||
}
|
||||
|
||||
.mar_top10 {
|
||||
margin-top: 20rpx !important;
|
||||
}
|
||||
|
||||
.mar_top15 {
|
||||
margin-top: 30rpx !important;
|
||||
}
|
||||
|
||||
.mar_top20 {
|
||||
margin-top: 40rpx !important;
|
||||
}
|
||||
|
||||
.mar_top25 {
|
||||
margin-top: 50rpx !important;
|
||||
}
|
||||
|
||||
/* 控制字体粗细 */
|
||||
.fw_blod {
|
||||
font-weight: bold !important;
|
||||
}
|
||||
|
||||
/* 控制背景色 */
|
||||
.bg_e8 {
|
||||
background-color: #e8e8e8 !important;
|
||||
}
|
||||
|
||||
/* 控制背景色 */
|
||||
.bg_cc {
|
||||
background-color: #CCCCCC !important;
|
||||
}
|
||||
|
||||
/* 控制背景色 */
|
||||
.bg_ff {
|
||||
background-color: #ffffff !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 弹性布局 */
|
||||
.fl_box {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.fl_deri {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.fl_row {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.fl_justmiddle {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.fl_juststart {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.fl_justbet {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.fl_justround {
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.fl_justend {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.fl_almiddle {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.fl_alstart {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.fl_alend {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.fl_1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.fl_warp {
|
||||
flex-wrap: wrap
|
||||
}
|
||||
|
||||
.fl_nowarp {
|
||||
flex-wrap: nowrap
|
||||
}
|
||||
|
||||
.line_2 {
|
||||
display: -webkit-box;
|
||||
/* 让文本内容成为弹性盒 */
|
||||
-webkit-box-orient: vertical;
|
||||
/* 设置盒子的方向为垂直 */
|
||||
-webkit-line-clamp: 2;
|
||||
/* 限制最多显示两行 */
|
||||
overflow: hidden;
|
||||
/* 隐藏超出的文本 */
|
||||
text-overflow: ellipsis;
|
||||
/* 使用省略号 */
|
||||
}
|
||||
|
||||
.line_1 {
|
||||
display: -webkit-box;
|
||||
/* 让文本内容成为弹性盒 */
|
||||
-webkit-box-orient: vertical;
|
||||
/* 设置盒子的方向为垂直 */
|
||||
-webkit-line-clamp: 1;
|
||||
/* 限制最多显示两行 */
|
||||
overflow: hidden;
|
||||
/* 隐藏超出的文本 */
|
||||
text-overflow: ellipsis;
|
||||
/* 使用省略号 */
|
||||
}
|
||||
589
common/globalFunction.js
Normal file
589
common/globalFunction.js
Normal file
@@ -0,0 +1,589 @@
|
||||
import useUserStore from "../stores/useUserStore";
|
||||
import {
|
||||
request,
|
||||
createRequest,
|
||||
uploadFile
|
||||
} from "../utils/request";
|
||||
import streamRequest, {
|
||||
chatRequest
|
||||
} from "../utils/streamRequest.js";
|
||||
|
||||
export const CloneDeep = (props) => {
|
||||
if (typeof props !== 'object' || props === null) {
|
||||
return props
|
||||
}
|
||||
|
||||
let result
|
||||
if (props) {
|
||||
result = []
|
||||
} else {
|
||||
result = {}
|
||||
}
|
||||
|
||||
for (let key in props) {
|
||||
if (props.hasOwnProperty(key)) {
|
||||
result[key] = CloneDeep(props[key])
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
export const msg = (title, duration = 1500, mask = false, icon = 'none', image) => {
|
||||
if (Boolean(title) === false) {
|
||||
return;
|
||||
}
|
||||
uni.showToast({
|
||||
title,
|
||||
duration,
|
||||
mask,
|
||||
icon,
|
||||
image
|
||||
});
|
||||
}
|
||||
|
||||
const prePage = () => {
|
||||
let pages = getCurrentPages();
|
||||
let prePage = pages[pages.length - 2];
|
||||
return prePage.$vm;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 页面跳转封装,支持 query 参数传递和返回回调
|
||||
* @param {string} url - 跳转路径
|
||||
* @param {object} options
|
||||
* @param {boolean} options.needLogin - 是否需要登录
|
||||
* @param {object} options.query - 携带参数
|
||||
* @param {function} options.onBack - 页面返回时的回调(目标页调用 uni.navigateBack 时传递数据)
|
||||
*/
|
||||
export const navTo = function(url, {
|
||||
needLogin = false,
|
||||
query = {},
|
||||
onBack = null
|
||||
} = {}) {
|
||||
const userStore = useUserStore();
|
||||
|
||||
if (needLogin && !userStore.hasLogin) {
|
||||
uni.navigateTo({
|
||||
url: '/pages/login/login'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const queryStr = Object.entries(query)
|
||||
.map(([key, val]) => `${key}=${encodeURIComponent(val)}`)
|
||||
.join('&');
|
||||
const finalUrl = queryStr ? `${url}?${queryStr}` : url;
|
||||
|
||||
if (onBack) {
|
||||
const pages = getCurrentPages();
|
||||
const currentPage = pages[pages.length - 1];
|
||||
currentPage.__onBackCallback__ = onBack;
|
||||
}
|
||||
|
||||
uni.navigateTo({
|
||||
url: finalUrl
|
||||
});
|
||||
};
|
||||
|
||||
export const navBack = function({
|
||||
delta = 1,
|
||||
data = null,
|
||||
fallbackUrl = '/pages/index/index'
|
||||
} = {}) {
|
||||
const pages = getCurrentPages();
|
||||
|
||||
if (pages.length > 1) {
|
||||
const prevPage = pages[pages.length - 1 - delta];
|
||||
|
||||
// 如果上一页存在回调函数,调用
|
||||
if (data && prevPage?.__onBackCallback__) {
|
||||
prevPage.__onBackCallback__(data);
|
||||
}
|
||||
|
||||
uni.navigateBack({
|
||||
delta
|
||||
});
|
||||
} else {
|
||||
// 没有可返回的页面,直接跳转 fallback 页面
|
||||
uni.reLaunch({
|
||||
url: fallbackUrl
|
||||
});
|
||||
}
|
||||
};
|
||||
// // 默认返回上一页
|
||||
// navBack();
|
||||
|
||||
// // 返回上两层
|
||||
// navBack(2);
|
||||
|
||||
// // 没有历史页面时跳转首页
|
||||
// navBack(1, '/pages/home/home');
|
||||
|
||||
function getdeviceInfo() {
|
||||
const globalData = {
|
||||
statusBarHeight: 0, // 状态导航栏高度
|
||||
topHeight: 0, // 距离顶部高度
|
||||
navHeight: 0, // 总体高度
|
||||
windowHeight: 0, // 可使用窗口高度
|
||||
tabBarHight: 0, //底部导航栏高度
|
||||
};
|
||||
let systemInfo = uni.getSystemInfoSync()
|
||||
globalData.windowHeight = systemInfo.screenHeight
|
||||
// 底部导航栏
|
||||
globalData.tabBarHight = systemInfo.screenHeight - systemInfo.safeArea.bottom
|
||||
// 状态栏高度
|
||||
globalData.statusBarHeight = systemInfo.statusBarHeight
|
||||
// #ifdef MP-MP-WEIXIN
|
||||
let menuButtonInfo = uni.getMenuButtonBoundingClientRect()
|
||||
// 胶囊距离顶部高度
|
||||
globalData.topHeight = menuButtonInfo.top
|
||||
// 胶囊高度
|
||||
globalData.navHeight = menuButtonInfo.height
|
||||
// #endif
|
||||
return {
|
||||
...globalData
|
||||
}
|
||||
}
|
||||
|
||||
function sleep(time) {
|
||||
return new Promise((resolve) => setTimeout(resolve, time))
|
||||
}
|
||||
const cloneDeep = (obj) => {
|
||||
// 1.1 判断是否是对象
|
||||
const isObject = (obj) => (typeof obj === 'object' || typeof obj === 'function') && obj !== 'null'
|
||||
|
||||
if (!isObject(obj)) {
|
||||
throw new Error('参数不是对象')
|
||||
}
|
||||
// 1.3 如果参数为数组,则复制数组各元素,否则复制对象属性
|
||||
const newObject = Array.isArray(obj) ? [...obj] : {
|
||||
...obj
|
||||
}
|
||||
// 1.4 迭代
|
||||
Object.keys(newObject).forEach((key) => {
|
||||
// 1.5 判断如果遍历到的属性值为对象,则继续递归cloneDeep
|
||||
if (isObject(newObject[key])) {
|
||||
newObject[key] = cloneDeep(newObject[key])
|
||||
}
|
||||
})
|
||||
return newObject
|
||||
}
|
||||
|
||||
const CopyText = (text) => {
|
||||
let input = document.createElement('textarea');
|
||||
input.value = text;
|
||||
document.body.appendChild(input);
|
||||
input.select();
|
||||
let flag = document.execCommand('copy')
|
||||
if (flag) {
|
||||
message.success('成功复制到剪贴板')
|
||||
} else {
|
||||
message.success('复制失败')
|
||||
}
|
||||
document.body.removeChild(input)
|
||||
}
|
||||
|
||||
// 柯里化 降低使用范围,提高适用性
|
||||
function Exp(regExp) {
|
||||
return (str) => {
|
||||
return regExp.test(str)
|
||||
}
|
||||
}
|
||||
|
||||
const checkingPhoneRegExp = Exp(/^1[3-9]{1}\d{9}/)
|
||||
// 手机号校验 checkingPhoneRegExp(phone)
|
||||
|
||||
const checkingEmailRegExp = Exp(/^[a-z0-9_\.-]+@[a-z0-9_\.-]+[a-z0-9]{2,6}$/i)
|
||||
// 邮箱校验 checkingEmailRegExp(email)
|
||||
|
||||
|
||||
function throttle(fn, delay = 300) {
|
||||
let valid = true
|
||||
let savedArgs = null // 参数存储器
|
||||
let savedContext = null // 上下文存储器
|
||||
|
||||
return function(...args) {
|
||||
// 保存当前参数和上下文
|
||||
savedArgs = args
|
||||
savedContext = this
|
||||
|
||||
if (!valid) return false
|
||||
valid = false
|
||||
setTimeout(() => {
|
||||
fn.apply(savedContext, savedArgs)
|
||||
valid = true
|
||||
savedArgs = null // 清空存储
|
||||
savedContext = null
|
||||
}, delay)
|
||||
}
|
||||
}
|
||||
|
||||
function debounce(fun, delay) {
|
||||
return function(args) {
|
||||
let that = this
|
||||
let _args = args
|
||||
clearTimeout(fun.id)
|
||||
fun.id = setTimeout(function() {
|
||||
fun.call(that, _args)
|
||||
}, delay)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function toRad(degree) {
|
||||
return degree * Math.PI / 180;
|
||||
}
|
||||
|
||||
function haversine(lat1, lon1, lat2, lon2) {
|
||||
const R = 6371; // 地球半径,单位为公里
|
||||
const a1 = toRad(lat1);
|
||||
const a2 = toRad(lat2);
|
||||
const b1 = toRad(lat2 - lat1);
|
||||
const b2 = toRad(lon2 - lon1);
|
||||
|
||||
const a = Math.sin(b1 / 2) * Math.sin(b1 / 2) +
|
||||
Math.cos(a1) * Math.cos(a2) * Math.sin(b2 / 2) * Math.sin(b2 / 2);
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
const distance = R * c; // 计算得到的距离,单位为公里
|
||||
|
||||
return distance;
|
||||
}
|
||||
|
||||
export function getDistanceFromLatLonInKm(lat1, lon1, lat2, lon2) {
|
||||
const R = 6371; // 地球平均半径,单位为公里
|
||||
const dLat = deg2rad(lat2 - lat1);
|
||||
const dLon = deg2rad(lon2 - lon1);
|
||||
const a =
|
||||
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||
Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) *
|
||||
Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
const d = R * c;
|
||||
return {
|
||||
km: d,
|
||||
m: d * 1000
|
||||
};
|
||||
}
|
||||
|
||||
// 将角度转换为弧度
|
||||
function deg2rad(deg) {
|
||||
return deg * (Math.PI / 180);
|
||||
}
|
||||
|
||||
function vacanciesTo(vacancies) {
|
||||
if (vacancies >= 0) {
|
||||
return vacancies + "人"
|
||||
} else {
|
||||
return '不限人数'
|
||||
}
|
||||
}
|
||||
|
||||
function salaryGlobal(type = 'min') {
|
||||
const salay = [2, 5, 10, 15, 20, 25, 30, 50, 80];
|
||||
const salaymax = [2, 5, 10, 15, 20, 25, 30, 50, 80, 100];
|
||||
const salarys = salay.map((item, index) => ({
|
||||
label: item + 'k',
|
||||
value: item * 1000,
|
||||
children: CloneDeep(salaymax).splice(index).map((vItem) => ({
|
||||
label: vItem + 'k',
|
||||
value: vItem * 1000,
|
||||
}))
|
||||
}))
|
||||
|
||||
return salarys
|
||||
}
|
||||
|
||||
class CustomSystem {
|
||||
constructor() {
|
||||
const systemInfo = uni.getSystemInfoSync();
|
||||
this.systemInfo = systemInfo
|
||||
}
|
||||
}
|
||||
const customSystem = new CustomSystem()
|
||||
|
||||
function setCheckedNodes(nodes, ids) {
|
||||
const isClear = ids.length === 0;
|
||||
|
||||
nodes.forEach((firstLayer) => {
|
||||
// 每次处理都先重置
|
||||
firstLayer.checkednumber = 0;
|
||||
|
||||
const traverse = (node) => {
|
||||
if (isClear) {
|
||||
node.checked = false;
|
||||
} else {
|
||||
node.checked = ids.includes(node.id);
|
||||
}
|
||||
|
||||
if (node !== firstLayer && node.checked) {
|
||||
firstLayer.checkednumber++;
|
||||
}
|
||||
|
||||
if (node.children && node.children.length) {
|
||||
node.children.forEach(child => traverse(child));
|
||||
}
|
||||
};
|
||||
|
||||
traverse(firstLayer);
|
||||
});
|
||||
|
||||
return nodes;
|
||||
}
|
||||
const formatTotal = (total) => {
|
||||
if (total < 10) return total.toString(); // 直接返回小于 10 的数
|
||||
|
||||
const magnitude = Math.pow(10, Math.floor(Math.log10(total))); // 计算数量级
|
||||
const roundedTotal = Math.floor(total / magnitude) * magnitude; // 去掉零头
|
||||
|
||||
return `${roundedTotal}+`;
|
||||
};
|
||||
|
||||
export function formatDate(isoString) {
|
||||
const date = new Date(isoString);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0'); // 月份从 0 开始,需要 +1
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
}
|
||||
|
||||
export function insertSortData(data, attribute = 'createTime') {
|
||||
const sortedData = data.sort((a, b) => new Date(b[attribute]) - new Date(a[attribute])); // 按时间降序排序
|
||||
const result = [];
|
||||
let lastDate = '';
|
||||
let lastTitle = ''
|
||||
|
||||
|
||||
const now = new Date();
|
||||
const todayStr = now.toISOString().split('T')[0]; // 获取今天的日期字符串
|
||||
const yesterday = new Date(now.setDate(now.getDate() - 1)).toISOString().split('T')[0]; // 获取昨天的日期字符串
|
||||
const twoDaysAgo = new Date(now.setDate(now.getDate() - 1)).toISOString().split('T')[0]; // 获取前天的日期字符串
|
||||
|
||||
sortedData.forEach(item => {
|
||||
const itemAttribute = item[attribute].replace('T', ' ')
|
||||
const itemDate = itemAttribute.split(' ')[0]; // 提取日期部分
|
||||
|
||||
let title = itemDate;
|
||||
if (itemDate === todayStr) {
|
||||
title = '今天';
|
||||
} else if (itemDate === yesterday) {
|
||||
title = '昨天';
|
||||
} else if (itemDate === twoDaysAgo) {
|
||||
title = '前天';
|
||||
}
|
||||
|
||||
if (lastDate !== itemDate) {
|
||||
result.push({
|
||||
title,
|
||||
isTitle: true
|
||||
});
|
||||
lastDate = itemDate;
|
||||
lastTitle = title;
|
||||
}
|
||||
|
||||
result.push({
|
||||
...item,
|
||||
isTitle: false
|
||||
});
|
||||
});
|
||||
|
||||
return [result, lastTitle];
|
||||
}
|
||||
|
||||
function getWeeksOfMonth(year, month) {
|
||||
const firstDay = new Date(year, month - 1, 1); // 当月第一天
|
||||
const lastDay = new Date(year, month, 0); // 当月最后一天
|
||||
const weeks = [];
|
||||
let week = [];
|
||||
|
||||
for (let d = new Date(firstDay); d <= lastDay; d.setDate(d.getDate() + 1)) {
|
||||
// 补充第一周的上个月日期
|
||||
if (week.length === 0 && d.getDay() !== 1) {
|
||||
let prevMonday = new Date(d);
|
||||
prevMonday.setDate(d.getDate() - (d.getDay() === 0 ? 6 : d.getDay() - 1));
|
||||
while (prevMonday < d) {
|
||||
week.push({
|
||||
year: prevMonday.getFullYear(),
|
||||
month: prevMonday.getMonth() + 1,
|
||||
day: prevMonday.getDate(),
|
||||
fullDate: getLocalYYYYMMDD(prevMonday), // 修正
|
||||
weekday: ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"][prevMonday.getDay()],
|
||||
isCurrent: false // 上个月日期
|
||||
});
|
||||
prevMonday.setDate(prevMonday.getDate() + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加当前月份的日期
|
||||
week.push({
|
||||
year: d.getFullYear(),
|
||||
month: d.getMonth() + 1,
|
||||
day: d.getDate(),
|
||||
fullDate: getLocalYYYYMMDD(d), // 修正
|
||||
weekday: ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"][d.getDay()],
|
||||
isCurrent: true // 当前月的日期
|
||||
});
|
||||
|
||||
// 如果到了月末但当前周未满7天,需要补足到周日
|
||||
if (d.getTime() === lastDay.getTime() && week.length < 7) {
|
||||
let nextDay = new Date(d);
|
||||
nextDay.setDate(d.getDate() + 1);
|
||||
while (week.length < 7) {
|
||||
week.push({
|
||||
year: nextDay.getFullYear(),
|
||||
month: nextDay.getMonth() + 1,
|
||||
day: nextDay.getDate(),
|
||||
fullDate: getLocalYYYYMMDD(nextDay), // 修正
|
||||
weekday: ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"][nextDay.getDay()],
|
||||
isCurrent: false // 下个月日期
|
||||
});
|
||||
nextDay.setDate(nextDay.getDate() + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果本周满了(7天)或者到了月末
|
||||
if (week.length === 7 || d.getTime() === lastDay.getTime()) {
|
||||
weeks.push([...week]); // 存入当前周
|
||||
week = []; // 清空,准备下一周
|
||||
}
|
||||
}
|
||||
|
||||
return weeks;
|
||||
}
|
||||
|
||||
// 新增工具函数:将日期格式化为本地 YYYY-MM-DD 字符串
|
||||
function getLocalYYYYMMDD(date) {
|
||||
const y = date.getFullYear();
|
||||
const m = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const d = String(date.getDate()).padStart(2, '0');
|
||||
return `${y}-${m}-${d}`;
|
||||
}
|
||||
|
||||
function isFutureDate(dateStr) {
|
||||
const inputDate = new Date(dateStr);
|
||||
const today = new Date();
|
||||
|
||||
// 只比较年月日,不考虑具体时间
|
||||
today.setHours(0, 0, 0, 0);
|
||||
inputDate.setHours(0, 0, 0, 0);
|
||||
|
||||
return inputDate > today;
|
||||
}
|
||||
|
||||
function parseQueryParams(url = window.location.href) {
|
||||
const queryString = url.split('?')[1]?.split('#')[0];
|
||||
const params = {};
|
||||
if (!queryString) return params;
|
||||
|
||||
queryString.split('&').forEach(param => {
|
||||
const [key, value] = param.split('=');
|
||||
if (key) {
|
||||
params[decodeURIComponent(key)] = decodeURIComponent(value || '');
|
||||
}
|
||||
});
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes < 1024) return bytes + ' B'
|
||||
else if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB'
|
||||
else if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(2) + ' MB'
|
||||
else return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB'
|
||||
}
|
||||
|
||||
|
||||
function sendingMiniProgramMessage(data = {
|
||||
text: 'hello'
|
||||
}, action = 'defalut') {
|
||||
jWeixin.miniProgram.postMessage({
|
||||
data,
|
||||
action
|
||||
});
|
||||
}
|
||||
|
||||
function copyText(text) {
|
||||
uni.setClipboardData({
|
||||
data: text,
|
||||
showToast: false,
|
||||
success(res) {
|
||||
msg('复制成功')
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function appendScriptTagElement(src) {
|
||||
if (!src) return null;
|
||||
return new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script');
|
||||
script.src = src;
|
||||
script.onload = () => {
|
||||
resolve()
|
||||
};
|
||||
script.onerror = () => {
|
||||
reject()
|
||||
};
|
||||
document.body.appendChild(script);
|
||||
})
|
||||
}
|
||||
|
||||
function isInWechatMiniProgramWebview() {
|
||||
const ua = navigator.userAgent.toLowerCase()
|
||||
return ua.includes('miniprogram') || window.__wxjs_environment === 'miniprogram'
|
||||
}
|
||||
|
||||
function isEmptyObject(obj) {
|
||||
return obj && typeof obj === 'object' && !Array.isArray(obj) && Object.keys(obj).length === 0;
|
||||
}
|
||||
|
||||
|
||||
export const $api = {
|
||||
msg,
|
||||
prePage,
|
||||
sleep,
|
||||
request,
|
||||
createRequest,
|
||||
streamRequest,
|
||||
chatRequest,
|
||||
insertSortData,
|
||||
uploadFile,
|
||||
formatFileSize,
|
||||
sendingMiniProgramMessage,
|
||||
copyText
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default {
|
||||
$api,
|
||||
navTo,
|
||||
navBack,
|
||||
cloneDeep,
|
||||
formatDate,
|
||||
getdeviceInfo,
|
||||
checkingPhoneRegExp,
|
||||
checkingEmailRegExp,
|
||||
throttle,
|
||||
debounce,
|
||||
haversine,
|
||||
getDistanceFromLatLonInKm,
|
||||
vacanciesTo,
|
||||
salaryGlobal,
|
||||
customSystem,
|
||||
setCheckedNodes,
|
||||
formatTotal,
|
||||
getWeeksOfMonth,
|
||||
isFutureDate,
|
||||
parseQueryParams,
|
||||
appendScriptTagElement,
|
||||
insertSortData,
|
||||
isInWechatMiniProgramWebview,
|
||||
isEmptyObject,
|
||||
}
|
||||
Reference in New Issue
Block a user