Преглед изворни кода

日志模块和权限管理功能处理

oyq28 пре 5 месеци
родитељ
комит
d349c10c51

+ 3 - 3
api/login.uts

@@ -30,7 +30,7 @@ export function login(username:string, password:string, code:string, uuid:string
     code: "",
     uuid: '1',
   	token: '123456',
-	roles:[] as string[]
+	roles:['admin']
   } as UTSJSONObject
   
     // #ifdef APP-ANDROID
@@ -74,9 +74,9 @@ export function register(data:RegForm):Promise<UTSJSONObject> {
     data
   }) */
   // #ifdef APP-ANDROID
-	let condition = 'username,password,token,status,createtime,updatetime'
+	let condition = 'username,password,token,role,status,createtime,updatetime'
 	let date = "strftime('%Y-%m-%d %H:%M:%S', 'now')"
-	let dataStr = `'${data.username}','${data.password}','${data.token}',1, ${date}, ${date}`;  
+	let dataStr = `'${data.username}','${data.password}','${data.token}','user',1, ${date}, ${date}`;  
 	console.log(dataStr)
 	const result = insertTableData('app_user', dataStr, condition);
 	console.log(result);

+ 13 - 1
api/work.uts

@@ -1,5 +1,5 @@
 // #ifdef APP-ANDROID
-import { selectTableData, insertTableData, updateTableData, selectJoinTableData, selectRecordData, selectRecordInfo, selectLatestInfoData, deleteTableData, selectTaskInfo, selectLatestTaskData, selectTaskId, selectTaskDetail, selectTableDataByOrder } from '@/utils/sqlite'
+import { selectTableData, insertTableData, updateTableData, selectJoinTableData, selectRecordData, selectRecordInfo, selectLatestInfoData, deleteTableData, selectTaskInfo, selectLatestTaskData, selectTaskId, selectTaskDetail, selectTableDataByOrder, pullSQL } from '@/utils/sqlite'
 // #endif
 
 export type Download = {
@@ -655,6 +655,18 @@ export async function getListByOrder(tableName : string, condition ?: string | n
 	return result;
 	// #endif
 
+	// #ifdef H5
+	return offlineData(initData)
+	// #endif
+}
+
+export async function selectPageSql(tableName : string, order : string, num : number):Promise<UTSJSONObject> {
+	// #ifdef APP-ANDROID
+	const result = await pullSQL(tableName, order, num);
+	console.log(result);
+	return result;
+	// #endif
+	
 	// #ifdef H5
 	return offlineData(initData)
 	// #endif

+ 14 - 0
pages.json

@@ -163,6 +163,20 @@
 		    "style" : {
 		        "navigationBarTitleText" : "拍照界面"
 		    }
+		},
+		{
+			"path" : "pages/work/log/logList",
+			"style" : 
+			{
+				"navigationBarTitleText" : "日志列表"
+			}
+		},
+		{
+			"path" : "pages/work/log/logDetail",
+			"style" : 
+			{
+				"navigationBarTitleText" : "日志详情"
+			}
 		}
     ],
     "tabBar" : {

+ 2 - 2
pages/mine/setting/index.uvue

@@ -1,7 +1,7 @@
 <template>
   <view class=" setting-container my-page">
     <view class=" menu-list">
-      <view class=" list-cell list-cell-arrow" @click="handleToPwd">
+      <view class=" list-cell list-cell-arrow" @click="handleToPwd" v-if="isAdmin==false">
         <view class=" menu-item-box uni-row">
           <view class=" iconfont icon-password menu-icon"></view>
           <view class="font-14">修改密码</view>
@@ -41,7 +41,7 @@
   export default {
     data() {
       return {
-		isAdmin: state.name=='admin'? true: false,  
+		isAdmin: state.roles.includes('admin')? true: false,  
         windowHeight: uni.getSystemInfoSync().windowHeight
       }
     },

+ 11 - 5
pages/work/index.uvue

@@ -24,7 +24,7 @@
 	<!-- 宫格 -->
     <view class="grid-body">
 	  <view class="grid uni-row">
-	        <view class="grid-item" v-for="(item, index) in items" :key="index" @click="enterItem(item)" >
+          <view class="grid-item" v-for="(item: Item, index: number) in items" :key="index" @click="enterItem(item)" >
 	          <!-- 您的网格项内容 -->
 	          <uni-icons class="my-icon" :type="item.iconType" size="40" :color="item.colorClass" ></uni-icons>
 	          <text class="text">{{ item.text }}</text> 
@@ -38,6 +38,7 @@
 </template>
 
 <script lang="uts">
+  import {state} from '@/store'; 	
   type ImageItem={
   	 image:string
   }
@@ -49,8 +50,7 @@
   }	
   export default {
     data() { 
-      return {
-		items: [
+	   let basic = [
 		      {colorClass: 'blue', iconType: "person-filled", text: "用户管理",path:"/pages/mine/index" },
 			  {colorClass: 'orange', iconType: "list", text: "检验任务",path:"/pages/work/download/DownloadList" },
 			  // {colorClass: 'blue', iconType: "list", text: "任务管理",path:"/pages/work/task/TaskList" },
@@ -59,8 +59,14 @@
 			  // {colorClass: 'orange', iconType: "gear-filled", text: "关键工序",path:"/pages/work/process/ProcessList" },
 			  // {colorClass: 'green', iconType: "folder-add-filled", text: "文档查阅",path:"" },
 			  // {colorClass: 'green', iconType: "cloud-upload", text: "数据上传",path:"" },
-			  {colorClass: 'green', iconType: "wallet-filled", text: "日志管理",path:"" }
-		] as Item[],
+			  //{colorClass: 'green', iconType: "wallet-filled", text: "日志管理",path:"/pages/work/log/logList" }
+		] as Item[];
+		let isAdmin = state.roles.includes('admin') ? true : false;
+		let adminItem = {colorClass: 'green', iconType: "wallet-filled", text: "日志管理",path:"/pages/work/log/logList" } as Item;
+		let items = isAdmin ? basic.concat(adminItem) : basic;
+		
+      return {
+		items: items as Item[],
         current: 0 as number,
         swiperDotIndex: 0 as number,
         data: [{

+ 270 - 0
pages/work/log/logDetail.uvue

@@ -0,0 +1,270 @@
+<template>
+	<!-- #ifdef APP -->
+	<scroll-view style="flex:1">
+	<!-- #endif -->
+		<!-- 报告基础信息 -->
+		<view class="section">
+			<view class="info-item">
+				<text class="label">日志内容</text>
+				<text class="value">{{ log.content}}</text>
+			</view>
+			<view class="info-item">
+				<text class="label">日志模块</text>
+				<text class="value">{{ log.module}}</text>
+			</view>	
+		</view>
+
+		<view class="section">
+			<uni-table>
+				<uni-tr class="section-title" v-for="(item,index) in titleList" :key="index">
+					<uni-td class="grid-text">{{item.title1}}</uni-td>
+					<uni-td class="grid-text">{{item.title2}}</uni-td>
+					<uni-td class="grid-text">{{item.title3}}</uni-td>
+				</uni-tr>
+				<uni-tr class="section-title" >
+					<uni-td class="grid-text">{{log.dataid ===0 ? "新增":"变更"}}</uni-td>
+					<uni-td class="grid-text">
+						<text :class="{
+							'bg-green bg-text': log.status === 1,
+							'bg-red bg-text': log.status === 0
+						  }">
+							{{log.status===1?"操作成功":"操作失败"}}
+						</text>
+					</uni-td>
+					<uni-td class="grid-text">
+						{{ log.dataid===0? log.createtime:log.updatetime }}
+					</uni-td>
+				</uni-tr>
+			</uni-table>
+		</view>
+		
+		<view class="section">
+			<view class="info-item">
+				<text class="label">日志传参</text>
+				<text class="value">{{ log.params}}</text>
+			</view>
+		</view>
+
+	<!-- #ifdef APP -->
+	</scroll-view>
+	<!-- #endif -->
+</template>
+
+<script setup>
+	import {
+		ref,
+		onMounted
+	} from 'vue'
+	import { getList, Download, TaskProcess, getRecordCalculate, RecordCalculate, statusDict } from '@/api/work';
+
+	//自定义返回行为,覆盖系统默认返回按钮
+	const backPressOptions = reactive({
+		from: 'backbutton'
+	} as OnBackPressOptions)
+	
+	const titleList = [{
+		title1: "类型", title2: "操作结果", title3: "操作时间"
+	}];
+	
+	type Log = {
+	  id : number,
+	  module : string,
+	  dataid : number,
+	  content : string,
+	  status : number,
+	  params : string,    
+	  updatetime : string,
+	  createtime: string   
+	}
+	
+	const log = ref<Log>({
+		id: 0,
+		module: "",
+		dataid: 0,
+		content: "",
+		status: 0,
+		params: "",	
+		updatetime: "",
+		createtime: "",		
+	})
+
+	// #ifdef H5
+	log.value = {
+		id: 1,
+		module: "app_media_info",
+		dataid: 0,
+		content : "新增数据成功",
+		params: "1CFA1040-00#S",    
+		createtime: "2025-06-23",
+		updatetime: "2025-06-23",	
+		status: 1
+  }
+
+	// #endif
+
+	onLoad((options) => {
+		const logId = options?.id ?? ""
+		// 模拟数据加载,建议替换为后端接口请求
+		// #ifdef APP-ANDROID
+		//获取下载产品数据			   
+		getList('app_log', 'id', logId, null, null, null).then((res : UTSJSONObject) => {
+			console.log(res)
+			let dataList = res?.['data'] as UTSJSONObject[] ?? Array<UTSJSONObject>()
+			if (dataList != null && dataList.length > 0) {
+				dataList.forEach(item => {
+					if (item != null) {
+						let data = JSON.parse<Log>(item.toJSONString());
+						if (data != null) {
+							log.value = data
+						}
+					}
+				});
+			}
+		})
+
+		// #endif
+	})
+
+	const goBack = () => {
+		uni.navigateBack()
+	}
+
+	defineExpose({
+		backPressOptions
+	})
+</script>
+
+<style scoped>
+	.container {
+		padding: 40rpx;
+		background-color: #f5f7fa;
+		flex: 1;
+		box-sizing: border-box;
+	}
+
+	.banner {
+		background: linear-gradient(135deg, #2193b0, #6dd5ed);
+		border-radius: 24rpx;
+		padding: 40rpx 30rpx;
+		margin-bottom: 40rpx;
+		box-shadow: 0 8rpx 16rpx rgba(33, 147, 176, 0.3);
+	}
+
+	.banner-title {
+		color: white;
+		font-size: 36rpx;
+		font-weight: bold;
+		text-align: center;
+	}
+
+	.section {
+		background-color: #fff;
+		border-radius: 20rpx;
+		padding: 30rpx;
+		margin-bottom: 30rpx;
+		box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.05);
+	}
+
+	.section-title {
+		display: flex;
+		flex-direction: row;
+		flex: 1;
+	}
+
+	.info-item {
+		display: flex;
+		justify-content: space-between;
+		/* #ifdef APP-NVUE */
+		font-size: 28rpx;
+		color: #666;
+		/* #endif */
+		margin-bottom: 18rpx;
+		flex-direction: row;
+	}
+
+	.section-content {
+		font-size: 28rpx;
+		color: #444;
+		line-height: 1.8;
+		white-space: normal;
+	}
+
+	.grid-text {
+		/* #ifdef APP-NVUE */
+		font-size: 24rpx;
+		color: #000;
+		/* #endif */
+		padding: 10rpx 0 10rpx 0rpx;
+		box-sizing: border-box;
+		margin: 5rpx 5rpx;
+		min-width: 200rpx;
+	}
+
+	.footer-btn {
+		margin-top: 40rpx;
+		display: flex;
+		justify-content: center;
+	}
+
+	.main-btn {
+		width: 80%;
+		padding: 28rpx 0;
+		font-size: 30rpx;
+		color: #fff;
+		border: none;
+		border-radius: 100rpx;
+		background: linear-gradient(to right, #36d1dc, #5b86e5);
+		box-shadow: 0 10rpx 24rpx rgba(91, 134, 229, 0.3);
+	}
+
+	.label {
+		font-weight: bold;
+		color: #102a43;
+		min-width: 150rpx;
+		margin-right: 30rpx;
+	}
+
+	.value {
+		flex: 1;
+		/* #ifdef APP-NVUE */
+		white-space: nowrap;
+		text-overflow: ellipsis;
+		/* #endif */
+		overflow: hidden;
+		margin-left: 30rpx;
+	}
+
+	.my-radius {
+		border-radius: 10rpx;
+	}
+
+	.bg-text {
+		width: 150rpx;
+		min-width: 150rpx;
+		border-radius: 10rpx;
+		text-align: center;
+	}
+
+	.bg-green {
+		background-color: seagreen;
+		color: #fff;
+	}
+
+	.bg-yellow {
+		background-color: yellow;
+	}
+
+	.bg-black {
+		background-color: #102a43;
+		color: #fff;
+	}
+
+	.bg-red {
+		background-color: red;
+		color: #fff;
+	}
+
+	.ft-red {
+		color: red;
+	}
+</style>

+ 319 - 0
pages/work/log/logList.uvue

@@ -0,0 +1,319 @@
+<template>
+  <view class="container">
+    <!-- 列表内容 -->
+    <!-- #ifdef APP -->
+    <list-view class="list" :bounces="false" :scroll-y="true" :custom-nested-scroll="true"
+        @scrolltolower="loadData(null)" associative-container="nested-scroll-view">
+	   <list-item class="list-item" type="10">	
+    <!-- #endif -->
+      <view class="download-card" v-for="(item, index) in logs" :key="index" >
+		<view @click="enterItem(item.id)">
+			<view class="info-row">
+			  <text class="label">数据表:</text>
+			  <text class="value">{{ item.module }}</text>
+			</view>
+			<view class="info-row">
+			  <text class="label">数据类型:</text>
+			  <text class="value">{{ item.dataid == 0 ? "新增" : "变更" }}</text>
+			</view>
+			<view class="info-row">
+			  <text class="label">日志内容:</text>
+			  <text class="value">{{ item.content }}</text>
+			</view>
+			<view class="info-row">
+			  <text class="label">操作结果:</text>
+			  <text class="value">{{ item.status === 1 ? "操作成功" : "操作失败" }}</text>
+			</view>			
+			<view class="info-row">
+			  <text class="label">操作时间:</text>
+			  <text class="value">{{ item.dataid === 0 ? item.createtime : item.updatetime }}</text>
+			</view>			
+		</view> 
+      </view>  
+    <!-- #ifdef APP -->
+	  </list-item type="20">
+	    <list-item class="loading">
+	          <uni-loading :loading="loading" color="#999" :text="loadingText"></uni-loading>
+	        </list-item>
+      </list-view>
+    <!-- #endif -->
+  </view>
+</template>
+
+<script setup>
+  import { ref, reactive } from 'vue'
+  import { getList, selectPageSql } from '@/api/work';
+  import { downloadDataFromAPI, uploadDataToAPI } from '@/utils/dataProcessor';
+  
+  const backPressOptions = reactive({
+    from: 'backbutton'
+  } as OnBackPressOptions)
+
+  onBackPress((options : OnBackPressOptions) : boolean | null => {
+    console.log('onBackPress', options)
+    // 使用reLaunch代替switchTab,避免多层跳转时的闪回问题
+    // reLaunch会关闭所有页面并打开到目标页面,适合需要完全重置导航栈的场景
+    uni.reLaunch({
+      url: `/pages/work/index`,
+    })
+    // 返回true表示拦截默认返回行为
+    return true
+  })
+
+  type Log = {
+    id : number,
+    module : string,
+    dataid : number,
+    content : string,
+    status : number,
+    params : string,    
+    updatetime : string,
+    createtime: string   
+  }
+
+  var initLog = [] as Log[]
+  var logs = ref<Log[]>([]);
+  const loading = ref(false)
+  const isEnded = ref(false)
+  const loadingError = ref('')
+  const currentPage = ref(1)
+  const map = ref(new Map<number, string>([[1, '未执行'], [2, '执行中'], [3, '执行完'], [4, '有错误']]))
+
+  const loadingText = computed((): string => {
+    if (loading.value) {
+      return "加载中..."
+    } else if (isEnded.value) {
+      return "没有更多了"
+    } else if (loadingError.value.length > 0) {
+      return loadingError.value
+    } else {
+      return ""
+    }
+  })
+  // #ifdef APP-ANDROID
+  
+  const loadData = (loadComplete : (() => void) | null) {
+    if (loading.value || isEnded.value) {
+      return
+    }
+    loading.value = true
+    let offset = currentPage.value === 1 ? 0 : (currentPage.value-1) * 10	
+  	selectPageSql('app_log', 'createtime', offset).then((res : UTSJSONObject) => {
+  		console.log(res)
+  		let dataList = res?.['data'] as UTSJSONObject[] ?? Array<UTSJSONObject>()
+  		if (dataList != null && dataList.length > 0) {
+  			dataList.forEach(item => {
+  			  if (item != null) {
+  			    let log = JSON.parse<Log>(item.toJSONString());
+  			    if (log != null) {
+  			      initLog.push(log)
+  			    }
+  			  }
+  			});
+			currentPage.value++			
+  		}else{
+			isEnded.value = true
+		}
+  		logs.value = initLog
+  		loading.value = false
+  		if (loadComplete != null) {
+  		    loadComplete()
+  		}
+  	}) 
+   
+  }
+  
+	
  onMounted(() => {
+	loadData(null)
+  })
+  
+  // #endif
+
+  // #ifdef H5
+  logs = [{
+    id: 1,
+    module: "app_media_info",
+    dataid: 0,
+	content : "新增数据成功",
+    params: "1CFA1040-00#S",    
+	createtime: "2025-06-23",
+    updatetime: "2025-06-23",	
+    status: 1
+  }, {
+    id: 2,
+    module: "app_media_info",
+    dataid: 0,
+	content : "新增数据成功",
+    params: "1CFA1040-00#S",    
+	createtime: "2025-06-23",
+    updatetime: "2025-06-23",	
+    status: 1
+  }] as Log[];
+
+  // #endif
+  
+  const enterItem = (id : number) => {
+    uni.navigateTo({
+      url: `/pages/work/log/logDetail?id=${id}`
+    });
+  }
+   
+  
+  defineExpose({
+	loadData,  
+    backPressOptions
+  })
+</script>
+
+<style scope>
+  .container {
+    padding: 24rpx;
+    background-color: #f0f4f8;
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    font-family: "PingFang SC", "Helvetica Neue", Helvetica, Arial, sans-serif;
+    /* #ifndef APP-ANDROID */
+    min-height: 100vh;
+    /* #endif */
+    height: 120rpx;
+  }
+
+  .search-bar {
+	  background-color: #ffffff;
+	  border-radius: 10rpx;
+	  box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
+}
+
+/* 搜索输入框:占满容器剩余空间,放在按钮左侧 */
+	.search-input {
+	  /* 清除默认输入框样式 */
+	  border: none;
+	  background: transparent;
+	  width: 70%;
+	  margin-left: 10rpx;
+	}
+
+/* 扫描按钮:放在输入框右侧,距离最右10rpx */
+	.scan-btn {
+	  justify-content: center;
+	  align-items: center; /* 新增:按钮内部图标垂直居中 */
+	  /* 关键:右侧10rpx边距,实现"距离最右10rpx" */
+	  margin-left: auto; /* 自动推到flex容器最右侧 */
+	  margin-right: 10rpx; /* 与容器右边缘保持10rpx间距 */
+	}
+
+  .download-card {
+    background: #ffffff;
+    border-radius: 20rpx;
+    padding: 24rpx 32rpx;
+    box-shadow: 0 8rpx 15rpx rgba(0, 43, 92, 0.1);
+    display: flex;
+    flex-direction: column;
+    margin-bottom: 20rpx;
+    margin-top: 40rpx;
+    margin-left: 20rpx;
+    margin-right: 20rpx;
+  }
+
+  .download-card .view {
+    margin-bottom: 16rpx;
+  }
+
+  /* 信息行 */
+  .info-row {
+    display: flex;
+    flex-direction: row;
+	/* #ifdef H5 */
+    font-size: 28rpx;
+    color: #33475b;
+	/* #endif */
+    align-items: center;
+  }
+
+  .info-row>.label {
+    margin-left: 10rpx;
+  }
+
+  .info-row>.value {
+    margin-left: 10rpx;
+  }
+
+  .btn-panel {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-left: 5rpx;
+    margin-right: 5rpx;
+  }
+
+  .label {
+    font-weight: bold;
+    color: #102a43;
+    min-width: 100rpx;
+  }
+
+  .value {
+    flex: 1;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+  }
+
+  .btn {
+    align-self: flex-end;
+    background-color: #00aaff;
+    color: #fff;
+    border: none;
+    border-radius: 32rpx;
+    padding: 2rpx 30rpx;
+    font-size: 24rpx;
+    font-weight: bold;
+    /* #ifndef APP-ANDROID */
+    transition: background-color 0.3s ease;
+    /* #endif */
+    margin-top: 30rpx;
+  }
+
+  .process-value {
+    width: 120rpx;
+    min-width: 100rpx;
+    text-align: center;
+    border-radius: 10rpx;
+  }
+
+  .bg-green {
+    background-color: seagreen;
+  }
+
+  .bg-yellow {
+    background-color: yellow;
+  }
+
+  .bg-black {
+    background-color: #102a43;
+  }
+
+  .btn-first {
+    margin-left: 5rpx;
+  }
+
+  .btn-second {
+    margin-right: 5rpx;
+	margin-left: auto;
+  }
+  .loading {
+      padding: 30px;
+      align-items: center;
+  }
+  .list {
+      flex: 1;
+      background-color: #ffffff;
+  }
+  .list-item {
+      flex-direction: column;
+      margin-top: 10px;
+      padding: 10px;
+  }
+  
+</style>

+ 6 - 1
store/index.uts

@@ -94,21 +94,26 @@ export const GetInfo=():Promise<UTSJSONObject>=> {
 	getInfo().then((res:UTSJSONObject) => {
 	  let avatar:string=profileImg as string
 	  let username:string="" as string
+	  let roles=[] as string[]
 	  const resMap=res.toMap() as Map<string,any>
 	   if(resMap.has("data")){
 		   const data:UTSJSONObject[]|null = resMap?.get("data") as UTSJSONObject[] ?? null;
 		   if(data!=null && data.length>0){
 			const user = data?.[0] as UTSJSONObject   
 			const userMap=user.toMap() as Map<string,any>
+			console.log(userMap)
 			if(userMap.has("avatar")&&user.getString("avatar")!=null&&user.getString("avatar")!=""){
 				avatar=globalConfig.baseUrl + user.getString("avatar") as string
 			}
 			if(userMap.has("username")&&user.getString("username")!=null&&user.getString("username")!=""){
 				username= user.getString("username") as string
 			}
+			if(userMap.has("role") &&user.getString("role")!=null&&user.getString("role")!=""){
+				roles.push(user.getString("role") as string)
+			}
 		   }
 	   }
-	   let roles=[] as string[]
+	   
 	   if(resMap.has("roles")){
 		   roles=res.getArray<string>("roles") as string[]
 		   if(roles.length>0){

+ 14 - 0
uni_modules/uni-loading/changelog.md

@@ -0,0 +1,14 @@
+## 1.0.6(2024-02-28)
+- 更新 Circle 组件名称,防止与内置组件名冲突
+## 1.0.5(2024-01-12)
+- 优化 删除组件内无用日志输出
+## 1.0.4(2024-01-10)
+- 优化 兼容 uvue h5 项目
+## 1.0.3(2023-12-22)
+- 更新 示例项目
+## 1.0.2(2023-12-22)
+- 优化 circle 部分动画逻辑,使动画更顺滑
+## 1.0.1(2023-12-20)
+- 修复 组件在高版本HBuilderX中运行出错的bug
+## 1.0.0(2023-12-19)
+- init

+ 70 - 0
uni_modules/uni-loading/components/uni-loading/icon.uvue

@@ -0,0 +1,70 @@
+<template>
+	<uni-icons :id="elId" class='load-ani' :style="aniStyle" @transitionend="onEnd" :type="iconType" :size="size" :color="color"></uni-icons>
+</template>
+
+<script>
+	export default {
+    name:'icon',
+		props: {
+			iconType: {
+				type: String,
+				default: 'loop'
+			},
+			size: {
+				type: Number,
+				default: 0
+			},
+			color: {
+				type: String,
+				default: '#333'
+			}
+		},
+		data() {
+			const elId = `Uni_${(Math.random() * 10e5).toInt().toString(36)}`
+			return {
+				elId: elId,
+				element: null as UniElement | null,
+				times: 0,
+				aniStyle: '',
+				deg: 3600000
+			}
+		},
+		created() {
+			this.times = 0
+
+			// 需要延迟一些时间,否则动画不生效
+			setTimeout(() => {
+				this.aniStyle = 'transform:rotate(1deg);'
+			}, 300)
+		},
+		mounted() {
+			this.element = uni.getElementById(this.elId as string)
+		},
+		methods: {
+			onEnd() {
+				// 因为循环角度是不断增加,在增加10次以后需要重置,防止无限增加下去
+				if (this.times == 10) {
+					this.element!.style.setProperty('transform', 'rotate(0deg)')
+					this.element!.style.setProperty('transition-duration', '1')
+					this.times = 0
+					return
+				}
+
+				this.times = this.times + 1
+
+				const rotate = this.times * 360
+				this.element!.style.setProperty('transform', 'rotate(' + rotate + 'deg)')
+				this.element!.style.setProperty('transition-duration', '1000')
+			}
+		}
+	}
+</script>
+
+<style>
+	.load-ani {
+		transition-property: transform;
+		transition-duration: 0.1s;
+		transition-timing-function: linear;
+		transform: rotate(0deg);
+	}
+</style>

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
uni_modules/uni-loading/components/uni-loading/load-img.uts


+ 186 - 0
uni_modules/uni-loading/components/uni-loading/loading-circle.uvue

@@ -0,0 +1,186 @@
+<template>
+  <!-- #ifdef APP -->
+  <view :ref="elId" class="block" :style="{width:size+'px',height:size+'px'}"></view>
+  <!-- #endif -->
+  <!-- #ifdef WEB -->
+  <svg :width="size" :height="size" viewBox="25 25 50 50" :style="{width:size+'px',height:size+'px'}" class="uni-load__img uni-load__img--android-H5">
+    <circle cx="50" cy="50" r="20" fill="none" :style="{color:color}" :stroke-width="iconsSize"></circle>
+  </svg>
+  <!-- #endif -->
+</template>
+<script>
+  import { easeInOutCubic } from './util'
+  let elId = 0
+  export default {
+    name: "circle",
+    props: {
+      speed: {
+        type: Number,
+        default: 16,
+      },
+      size: {
+        type: Number,
+        default: 20,
+      },
+      color: {
+        type: String,
+        default: '#666',
+      }
+    },
+    data() {
+      // 防止多调用,随机元素id
+      elId += 1
+      const elID = `Uni_Load_Circle_${elId}`
+      return {
+        elId: elID,
+        timer: 0,
+      };
+    },
+    computed: {
+      iconsSize() : number {
+        return (this.size / 10) +1
+      }
+    },
+    mounted() {
+      // #ifdef APP
+      this.init()
+      // #endif
+    },
+    unmounted() {
+      // 组件卸载时,需要卸载定时器,优化性能,防止页面卡死
+      clearInterval(this.timer)
+    },
+    methods: {
+      /**
+       * 初始化圆环
+       */
+      init() {
+        const refs = this.$refs[this.elId] as UniElement
+        let ctx = refs.getDrawableContext()!
+        this.build_circular(ctx)
+      },
+
+      /**
+       * 构建圆环动画
+       */
+      build_circular(ctx : DrawableContext) {
+        let startAngle = 0;
+        let rotate = 0;
+        const ARC_LENGTH = 359;
+        const center = this.size / 2; // 圆心
+        const lineWidth = this.size / 10; // 圆环宽度
+        const duration = 1200; // 动画持续时间
+        const interval = this.speed; // 定时器间隔(大约 60 帧每秒)
+
+        // 使圆环过度更自然,不必运动到底
+        const ARC_MAX = 358
+        let startTime = 0;
+        let foreward_end = 0 // 正传
+        let reversal_end = ARC_MAX // 反转
+        function pogress_time() : number {
+          const currentTime = Date.now();
+          // 运动时间计算
+          const elapsedTime = currentTime - startTime;
+          const progress = elapsedTime / duration;
+          // 动画缓动
+          const easedProgress = easeInOutCubic(progress);
+          return easedProgress
+        }
+        const draw = () => {
+
+          ctx.reset();
+          ctx.beginPath();
+
+          if (reversal_end == ARC_MAX) {
+            foreward_end = Math.min(pogress_time() * ARC_LENGTH, ARC_LENGTH); // 限制 end 的最大值为 ARC_LENGTH
+            if (foreward_end >= ARC_MAX) {
+              reversal_end = 0
+              foreward_end = ARC_MAX
+              startTime = Date.now();
+            }
+          }
+
+          if (foreward_end == ARC_MAX) {
+            reversal_end = Math.min(pogress_time() * ARC_LENGTH, ARC_LENGTH);
+            if (reversal_end >= ARC_MAX) {
+              reversal_end = ARC_MAX
+              foreward_end = 0
+              startTime = Date.now();
+            }
+          }
+
+          ctx.arc(
+            center,
+            center,
+            center - lineWidth,
+            startAngle + rotate + (reversal_end * Math.PI / 180),
+            startAngle + rotate + (foreward_end * Math.PI / 180)
+          );
+          ctx.lineWidth = lineWidth;
+          const fillColor = (this.color !== '' ? this.color : '#666').toString();
+          ctx.strokeStyle = fillColor;
+          ctx.stroke();
+          ctx.update();
+          rotate += 0.05; // 旋转速度
+        }
+
+        this.timer = setInterval(() => draw(), interval);
+      }
+
+    }
+  }
+</script>
+
+<style scoped>
+  .block {
+    width: 50px;
+    height: 50px;
+  }
+
+  /* #ifdef WEB */
+  .uni-load__img {
+    width: 24px;
+    height: 24px;
+  }
+
+  .uni-load__img--android-H5 {
+    animation: loading-android-H5-rotate 2s linear infinite;
+    transform-origin: center center;
+  }
+
+  .uni-load__img--android-H5 circle {
+    display: inline-block;
+    animation: loading-android-H5-dash 1.5s ease-in-out infinite;
+    stroke: currentColor;
+    stroke-linecap: round;
+  }
+
+  @keyframes loading-android-H5-rotate {
+    0% {
+      transform: rotate(0deg);
+    }
+
+    100% {
+      transform: rotate(360deg);
+    }
+  }
+
+  @keyframes loading-android-H5-dash {
+    0% {
+      stroke-dasharray: 1, 200;
+      /* stroke-dashoffset: 0; */
+    }
+
+    50% {
+      stroke-dasharray: 90, 150;
+      stroke-dashoffset: -20;
+    }
+
+    100% {
+      stroke-dasharray: 90, 150;
+      stroke-dashoffset: -120;
+    }
+  }
+
+  /* #endif */
+</style>

+ 164 - 0
uni_modules/uni-loading/components/uni-loading/uni-loading.uvue

@@ -0,0 +1,164 @@
+<template>
+	<!-- 如果没有插槽,则使用 load-inline 样式 -->
+	<view class="uni-loading-main" :class="{'load-inline':$slots['default'] == null}">
+		<template v-if="loading">
+			<slot></slot>
+			<template v-if="$slots['default'] == null">
+				<LoadingCircle :speed="16" :size="loadWidth" :color="color"></LoadingCircle>
+				<text v-if="text" class="inline-text" :style=" { color: color }">{{text}}</text>
+			</template>
+			<template v-else>
+				<view class="uni-loading-mask" :style="{backgroundColor:background}">
+					<LoadingCircle :speed="16" :size="loadWidth" :color="color"></LoadingCircle>
+					<text v-if="text" class="block-text" :style=" { color: color }">{{text}}</text>
+				</view>
+			</template>
+		</template>
+		<template v-else>
+			<slot></slot>
+		</template>
+	</view>
+</template>
+<script>
+	import LoadingCircle from './loading-circle.uvue'
+  // TODO 性能问题,其他类型暂时不对外开放
+	// import Icon from './icon.uvue'
+	// import UniIcons from '@/uni_modules/uni-icons/components/uni-icons/uni-icons.uvue'
+
+	// import { img_load_base } from './load-img.uts'
+
+	/**
+	 * Loading-x 加载动画
+	 * @description 用于数据加载场景,使用loading等待数据返回
+	 * @tutorial https://ext.dcloud.net.cn/plugin?name=uni-loading-x
+	 * @property {Boolean} loading 是否显示加载动画,默认:true
+	 * @property {String} type = [snow|circle|icon] 加载图标显示,默认:circle
+	 * 	@value snow 显示雪花加载动画,性能问题暂时不支持
+	 * 	@value circle 显示圆形加载动画
+	 * 	@value icon 自定义图标 ,暂时不支持
+	 * @property {String} background 加载遮罩颜色,支持 rgba 色值,默认:rgba(255,255,255,0.6)
+	 * @property {String} color 加载图标以及加载文字颜色,默认:#333333
+	 * @property {String} size 加载图标大小,默认:20
+	 * @property {String} text 加载文本,默认:不显示
+	 * @property {String} iconType 自定义图标类型,参考 uni-icons ,当前版本暂不支持
+	 */
+
+	export default {
+		name: "uni-loading",
+		components: { LoadingCircle },
+		props: {
+			loading: {
+				type: Boolean,
+				default: true,
+			},
+			type: {
+				type: String,
+				default: ''
+			},
+			iconType: {
+				type: String,
+				default: 'gear-filled'
+			},
+			size: {
+				type: Number,
+				default: 0
+			},
+			text: {
+				type: String,
+				default: ''
+			},
+			background: {
+				type: String,
+				default: 'rgba(255,255,255,0.6)'
+			},
+			color: {
+				type: String,
+				default: '#333'
+			}
+		},
+		data() {
+			return {};
+		},
+		computed: {
+			loadWidth() : number {
+				let width = this.size
+				if (width == 0) {
+					return 20
+				}
+				return width
+			},
+			styles() : string {
+				return `width:${this.loadWidth}px;height:${this.loadWidth}px;`
+			}
+		},
+		created() {},
+		methods: {}
+	}
+</script>
+
+<style scoped>
+	.uni-loading-main {
+		position: relative;
+	}
+
+	.uni-loading-main.load-inline {
+		display: flex;
+		flex-direction: row;
+		align-items: center;
+	}
+
+	.block-text {
+		margin-top: 8px;
+		font-size: 14px;
+	}
+
+	.inline-text {
+		margin-left: 8px;
+		font-size: 14px;
+	}
+
+
+	.uni-loading-mask {
+		position: absolute;
+		width: 100%;
+		height: 100%;
+		top: 0;
+		left: 0;
+		display: flex;
+		justify-content: center;
+		align-items: center;
+		justify-content: center;
+	}
+
+	.uni-loading-mask {
+		background-color: rgba(0, 0, 0, 0.3);
+		z-index: 2;
+	}
+
+	.uni-load {
+		display: flex;
+		flex-direction: row;
+		justify-content: center;
+		align-items: center;
+	}
+
+	.load-text {
+		font-size: 14px;
+		color: #fff;
+		margin-top: 12px;
+	}
+
+	.uni-load .image,
+	.load-image {
+		width: 100%;
+		height: 100%;
+	}
+
+	.load-ani {
+		transition-property: transform;
+		transition-duration: 0.1s;
+		transition-timing-function: linear;
+		transform: rotate(0deg);
+	}
+
+</style>

+ 42 - 0
uni_modules/uni-loading/components/uni-loading/util.uts

@@ -0,0 +1,42 @@
+
+/**
+ * hex颜色转rgba
+ */
+export const hexToRgba = (hex : string, alpha : number) : string => {
+	// 去除 # 符号(如果有的话)
+	hex = hex.replace('#', '');
+	let hexArray = hex.split('');
+	// 检查颜色值长度,如果不符合预期则返回默认值或者抛出错误
+	if (hexArray.length != 3 && hexArray.length != 6) {
+		// 返回默认值或者抛出错误,这里使用默认值为黑色
+		return 'rgba(0,0,0,1)';
+		// 或者抛出错误
+		// throw new Error('Invalid hex color value');
+	}
+
+	let extendedHex : string[] = [];
+
+	if (hex.length == 3) {
+		for (let i = 0; i < hexArray.length; i++) {
+			extendedHex.push(hexArray[i]);
+			extendedHex.push(hexArray[i]);
+		}
+		hexArray = extendedHex;
+	}
+	hex = ''
+	for (let h = 0; h < hexArray.length; h++) {
+		hex += hexArray[h]
+	}
+
+	// // 拆分颜色值为 R、G、B
+	const r = parseInt(hex.substring(0, 2), 16);
+	const g = parseInt(hex.substring(2, 4), 16);
+	const b = parseInt(hex.substring(4, 6), 16);
+
+	// // 返回 rgba 值
+	return `rgba(${r},${g},${b},${alpha})`;
+}
+
+export const easeInOutCubic = (t : number) : number => {
+	return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1;
+}

+ 88 - 0
uni_modules/uni-loading/package.json

@@ -0,0 +1,88 @@
+{
+	"id": "uni-loading",
+	"displayName": "uni-loading",
+	"version": "1.0.6",
+	"description": "加载动画组件多用在页面内数据加载时,提供一个loading动画,列表的上拉加载,下拉刷新等都需要加载动画",
+	"keywords": [
+        "loading",
+        "加载动画",
+        "上拉刷新",
+        "下拉加载"
+    ],
+	"repository": "",
+	"engines": {
+		"HBuilderX": "^3.97"
+	},
+	"dcloudext": {
+		"type": "component-vue",
+		"sale": {
+			"regular": {
+				"price": "0.00"
+			},
+			"sourcecode": {
+				"price": "0.00"
+			}
+		},
+		"contact": {
+			"qq": ""
+		},
+		"declaration": {
+			"ads": "无",
+			"data": "无",
+			"permissions": "无"
+		},
+		"npmurl": ""
+	},
+	"uni_modules": {
+		"dependencies": [
+			"uni-icons"
+		],
+		"encrypt": [],
+		"platforms": {
+			"cloud": {
+				"tcb": "y",
+				"aliyun": "y",
+				"alipay": "y"
+			},
+			"client": {
+				"Vue": {
+					"vue2": "n",
+					"vue3": "y"
+				},
+				"App": {
+					"app-vue": "n",
+					"app-nvue": "n",
+					"app-uvue": "y"
+				},
+				"H5-mobile": {
+					"Safari": "n",
+					"Android Browser": "n",
+					"微信浏览器(Android)": "n",
+					"QQ浏览器(Android)": "n"
+				},
+				"H5-pc": {
+					"Chrome": "n",
+					"IE": "n",
+					"Edge": "n",
+					"Firefox": "n",
+					"Safari": "n"
+				},
+				"小程序": {
+					"微信": "n",
+					"阿里": "n",
+					"百度": "n",
+					"字节跳动": "n",
+					"QQ": "n",
+					"钉钉": "n",
+					"快手": "n",
+					"飞书": "n",
+					"京东": "n"
+				},
+				"快应用": {
+					"华为": "n",
+					"联盟": "n"
+				}
+			}
+		}
+	}
+}

+ 76 - 0
uni_modules/uni-loading/readme.md

@@ -0,0 +1,76 @@
+
+# uni-loading 动画加载
+
+动画加载组件使用场景非常多,如在页面内数据加载时,提供一个loading动画,列表的上拉加载,下拉刷新中也需要加载动画。
+
+**注意:当前版本只支持 uni-app x**
+
+## 使用组件
+
+### 基本用法
+
+在 ``template`` 中使用组件,独立显示加载图标和加载文本
+
+```html
+<!-- 只显示加载图标 -->
+<uni-loading></uni-loading>
+<!-- 自定义加载图标颜色 -->
+<uni-loading color="#409EFF"></uni-loading>
+<!-- 修改加载图标大小 -->
+<uni-loading :size="30"></uni-loading>
+<!-- 修改加载图标类型 : 当前只支持 circle-->
+<uni-loading type="circle"></uni-loading>
+
+```
+
+### 覆盖元素的加载动画
+
+在 ``template`` 中使用组件,在原始布局元素上面覆盖遮罩和加载动画
+
+```html
+<uni-loading type="circle">
+	<text>第一行文本</text>
+	<text>第二行文本</text>
+	<text>第三行文本</text>
+</uni-loading>
+```
+**注意:原理是在原始元素外增加一层 view 节点,可能会影响页面布局,如发生布局影响,直接在 <uni-loading> 上修改样式即可,结构等同于:**
+
+```html
+<view class="uni-loading">
+	<text>第一行文本</text>
+	<text>第二行文本</text>
+	<text>第三行文本</text>
+</view>
+```
+
+
+### 取消加载动画
+
+使用 `loading` 属性可以关闭加载动画,为了优化组件性能,建议组件在页面不可见时,设置 `loading:false`,关闭加载动画
+
+
+```html
+<uni-loading :loading="false"></uni-loading>
+```
+
+## 属性/方法
+
+### Loading Props
+
+|属性名		|类型	|默认值					|说明										|
+|:-:		|:-:	|:-:					|:-:										|
+|loading	|Boolean|true					|是否显示加载动画								|
+|type		|String	|circle					|加载图标显示类型,当前只支持circle,其他类型或者自定义获取提供	|
+<!-- |iconType	|String	|loop					|自定义图标类型,值详见[uni-icons](https://uniapp.dcloud.net.cn/component/uniui/uni-icons.html#%E5%9B%BE%E6%A0%87%E7%A4%BA%E4%BE%8B)	| -->
+|background	|String	|rgba(255,255,255,0.6)	|加载动画遮罩颜色								|
+|color		|String	|#333333				|加载图标以及加载文字颜色						|
+|size		|Number	|20						|加载图标大小									|
+|text		|String	|-						|加载文本										|
+
+
+
+### Loading Slot
+|插槽名	|说明		|
+|:-:		|:-:		|
+|default|默认插槽	|

+ 2 - 1
utils/sqlite.uts

@@ -319,7 +319,7 @@ export function selectTableData (
    *
    */
   export function pullSQL(dbTable: string, id: string, num: number) : Promise<UTSJSONObject> {
-	var sql = `SELECT * FROM ${dbTable} ORDER BY '${id}' DESC LIMIT 15 OFFSET '${num}'` 
+	var sql = `SELECT * FROM ${dbTable} ORDER BY '${id}' DESC LIMIT 10 OFFSET '${num}'` 
 	const sqlite = createSQLiteContext(dbName); 
     return new Promise((resolve, reject) => {
       const selectSqlOptions ={
@@ -340,6 +340,7 @@ export function selectTableData (
       		  errMsg: info?.errMsg ?? '',
       		  data: info?.data ?? ['']
       } as UTSJSONObject
+	  sqlite.close();
       resolve(ret)
     })
   }

Неке датотеке нису приказане због велике количине промена