storagemanage.uvue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  1. <template>
  2. <view class="container">
  3. <view class="header">
  4. <text class="uni-title-text">Storage管理器</text>
  5. <button class="btn btn-create" type="primary" @click="openEditDialogNew">新建</button>
  6. <button class="btn btn-clear" type="default" @click="confirmClear" v-if="storageList.length>0">清空所有</button>
  7. </view>
  8. <list-view class="list-view" v-if="storageList.length>0">
  9. <list-item v-for="(item, index) in storageList" :key="item.key" @click="showDetail(item)">
  10. <view class="item-block">
  11. <view class="item-row">
  12. <text class="item-label">Key:</text>
  13. <text class="item-key">{{ item.key }}</text>
  14. </view>
  15. <view class="item-row">
  16. <text class="item-label">Data:</text>
  17. <text class="item-key">{{ truncate(item.value) }}</text>
  18. </view>
  19. <view class="item-row">
  20. <text class="item-label">Type:</text>
  21. <text class="item-key">{{ item.type }}</text>
  22. </view>
  23. <view class="item-row item-actions-row">
  24. <button class="btn btn-delete" type="default" @click.stop="confirmDelete(index)">删除</button>
  25. <button class="btn btn-edit" type="primary" @click.stop="openEditDialogEdit(item, index)">编辑</button>
  26. </view>
  27. </view>
  28. </list-item>
  29. </list-view>
  30. <view v-else class="uni-center">
  31. <text class="uni-hello-text">暂无数据</text>
  32. </view>
  33. <!-- 详情弹窗 -->
  34. <view v-if="showDetailDialog" class="dialog-mask" @click="closeDetail">
  35. <view class="dialog-content" @click.stop>
  36. <text class="dialog-title">详情</text>
  37. <view class="detail-row">
  38. <text class="item-label">Key:</text>
  39. <text class="item-key">{{ detailItem.key }}</text>
  40. </view>
  41. <view class="detail-row">
  42. <text class="item-label">Data:</text>
  43. <text class="item-key uni-list-cell-db-text">{{ detailItem.value }}</text>
  44. </view>
  45. <view class="detail-row">
  46. <text class="item-label">Type:</text>
  47. <text class="item-key">{{ detailItem.type }}</text>
  48. </view>
  49. <view class="uni-common-mt popup-actions">
  50. <button class="btn mr-20" type="primary" @click="openEditDialogEdit(detailItem, getDetailIndex())">编辑</button>
  51. <button class="btn mr-20" type="warn" @click="confirmDelete(getDetailIndex())">删除</button>
  52. <button class="btn" @click="closeDetail">关闭</button>
  53. </view>
  54. </view>
  55. </view>
  56. <!-- 新建/编辑弹窗 -->
  57. <view v-show="showEditDialog" class="dialog-mask" @click="closeEdit">
  58. <view class="dialog-content" @click.stop>
  59. <text class="dialog-title">{{ isEditing ? '编辑' : '新建' }}</text>
  60. <view class="edit-row">
  61. <text class="edit-label">Key</text>
  62. <input v-model="editKey" placeholder="请输入key" class="edit-input" />
  63. </view>
  64. <view class="edit-row">
  65. <text class="edit-label">Value</text>
  66. <textarea v-model="editValue" placeholder="请输入value" class="edit-textarea" />
  67. </view>
  68. <view class="edit-row" v-if="!isEditing">
  69. <text class="edit-label">类型</text>
  70. <radio-group class="edit-type-group" @change="onValueTypeChange">
  71. <radio v-for="vt in valueTypeOptions" :key="vt" :value="vt" :checked="editValueType===vt"
  72. class="edit-type-radio">
  73. <text>{{ vt }}</text>
  74. </radio>
  75. </radio-group>
  76. </view>
  77. <view class="popup-actions">
  78. <button class="btn mr-20 btn-cancel" type="default" @click="closeEdit">取消</button>
  79. <button class="btn btn-save" type="primary" @click="saveEdit">保存</button>
  80. </view>
  81. </view>
  82. </view>
  83. </view>
  84. </template>
  85. <script setup lang="uts">
  86. type StorageItem = {
  87. key : string
  88. value : string
  89. type : string
  90. }
  91. type StorageList = Array<StorageItem>
  92. const storageList = ref([] as StorageItem[])
  93. const newKey = ref('')
  94. const newValue = ref('')
  95. const isEditing = ref(false)
  96. const editIndex = ref(-1)
  97. const detailItem = ref({ key: '', value: '', type: '' } as StorageItem)
  98. const editKey = ref('')
  99. const editValue = ref('')
  100. const editValueType = ref('Number')
  101. const showDetailDialog = ref(false)
  102. const showEditDialog = ref(false)
  103. const valueTypeOptions = ['String', 'Number', 'Boolean', 'Object', 'Array']
  104. const valueTypeDefaultMap = new Map<string, string>()
  105. valueTypeDefaultMap.set('String', '')
  106. valueTypeDefaultMap.set('Number', '1')
  107. valueTypeDefaultMap.set('Boolean', 'true')
  108. valueTypeDefaultMap.set('Object', `{"name": "张三","age": 12}`)
  109. valueTypeDefaultMap.set('Array', `[1, "hello", true, { "key": "value" }]`)
  110. // 自动化测试使用
  111. const isTestMode = ref(false)
  112. function getStorageList() : StorageList {
  113. const list : StorageList = []
  114. const storageInfo = uni.getStorageInfoSync()
  115. storageInfo.keys.forEach((key : string) => {
  116. const value = uni.getStorageSync(key)
  117. let strValue : string | null = null
  118. let typeStr : string = typeof value
  119. if (value != null) {
  120. if (typeStr == 'object') {
  121. const jsonStr = JSON.stringify(value)
  122. strValue = jsonStr
  123. if (Array.isArray(JSON.parse(jsonStr))) {
  124. typeStr = 'Array'
  125. } else {
  126. typeStr = 'Object'
  127. }
  128. } else if (typeStr == 'boolean') {
  129. strValue = value == true ? 'true' : 'false'
  130. typeStr = 'Boolean'
  131. } else if (typeStr == 'number') {
  132. strValue = value.toString()
  133. typeStr = 'Number'
  134. } else {
  135. strValue = value.toString()
  136. typeStr = 'String'
  137. }
  138. }
  139. if (strValue != null) {
  140. list.push({
  141. key: key,
  142. value: strValue,
  143. type: typeStr
  144. })
  145. }
  146. })
  147. return list
  148. }
  149. function setStorage(key : string, value : any) {
  150. try {
  151. uni.setStorageSync(key, value)
  152. } catch (e) {
  153. console.error('Storage set error:', e)
  154. }
  155. }
  156. function removeStorage(key : string) {
  157. try {
  158. uni.removeStorageSync(key)
  159. } catch (e) {
  160. console.error('Storage remove error:', e)
  161. }
  162. }
  163. function clearStorage() {
  164. try {
  165. uni.clearStorageSync()
  166. } catch (e) {
  167. console.error('Storage clear error:', e)
  168. }
  169. }
  170. function getStorage(key : string) : string | null {
  171. try {
  172. const value = uni.getStorageSync(key)
  173. return value != null ? value.toString() : null
  174. } catch (e) {
  175. console.error('Storage get error:', e)
  176. return null
  177. }
  178. }
  179. function refreshList() {
  180. const list = getStorageList()
  181. console.log('list: ',list);
  182. if (!isEditing.value && editKey.value != '') {
  183. const idx = list.findIndex(item => item.key === editKey.value)
  184. if (idx > 0) {
  185. const spliced = list.splice(idx, 1)
  186. if (spliced.length > 0) {
  187. list.unshift(spliced[0])
  188. }
  189. }
  190. }
  191. storageList.value = list
  192. }
  193. function truncate(value : string) : string {
  194. if (value.length > 100) {
  195. return value.slice(0, 100) + '...'
  196. }
  197. return value
  198. }
  199. function showDetail(item : StorageItem) {
  200. detailItem.value = item
  201. showDetailDialog.value = true
  202. }
  203. function closeDetail() {
  204. showDetailDialog.value = false
  205. }
  206. function openEditDialogNew() {
  207. editKey.value = ''
  208. editValueType.value = 'String'
  209. editValue.value = valueTypeDefaultMap.get('String') ?? ''
  210. isEditing.value = false
  211. editIndex.value = -1
  212. showEditDialog.value = true
  213. }
  214. function openEditDialogEdit(item : StorageItem, index : number) {
  215. editKey.value = item.key
  216. editValue.value = item.value
  217. isEditing.value = true
  218. editIndex.value = index
  219. editValueType.value = valueTypeOptions.indexOf(item.type) >= 0 ? item.type : 'String'
  220. showEditDialog.value = true
  221. closeDetail()
  222. }
  223. function saveEdit() {
  224. if (!isTestMode.value && (editKey.value.trim() === '' || editValue.value.trim() === '')) {
  225. uni.showModal({
  226. title: '提示',
  227. content: 'Key 和 Value 都不能为空',
  228. showCancel: false,
  229. })
  230. return
  231. }
  232. let storeValue : any
  233. switch (editValueType.value) {
  234. case 'Number':
  235. storeValue = parseFloat(editValue.value)
  236. break
  237. case 'Boolean':
  238. storeValue = (editValue.value === 'true' || editValue.value === '1')
  239. break
  240. case 'Object':
  241. try {
  242. const obj = JSON.parse(editValue.value)
  243. storeValue = obj as UTSJSONObject
  244. } catch {
  245. storeValue = {} as UTSJSONObject
  246. }
  247. break
  248. case 'Array':
  249. try {
  250. const arr = JSON.parse(editValue.value) as Array<any>
  251. storeValue = arr // 直接存储数组
  252. } catch {
  253. storeValue = [] as Array<any>
  254. }
  255. break
  256. default:
  257. storeValue = editValue.value
  258. }
  259. if (editIndex.value >= 0) {
  260. const oldItem = storageList.value[editIndex.value]
  261. if (oldItem.key != editKey.value) {
  262. removeStorage(oldItem.key)
  263. }
  264. setStorage(editKey.value, storeValue)
  265. } else {
  266. setStorage(editKey.value, storeValue)
  267. }
  268. refreshList()
  269. isEditing.value = false
  270. editIndex.value = -1
  271. editKey.value = ''
  272. editValue.value = ''
  273. editValueType.value = 'String'
  274. showEditDialog.value = false
  275. }
  276. function closeEdit() {
  277. showEditDialog.value = false
  278. }
  279. function handleDelete(index : number) {
  280. if (index >= 0 && index < storageList.value.length) {
  281. const item = storageList.value[index]
  282. removeStorage(item.key)
  283. refreshList()
  284. if (isEditing.value && editIndex.value == index) {
  285. isEditing.value = false
  286. editIndex.value = -1
  287. editKey.value = ''
  288. editValue.value = ''
  289. }
  290. }
  291. }
  292. function confirmDelete(index : number) {
  293. // 自动化测试时不显示模态弹窗
  294. if (!isTestMode.value) {
  295. uni.showModal({
  296. title: '确认操作',
  297. content: '确定要删除该项吗?',
  298. showCancel: true,
  299. cancelText: '取消',
  300. confirmText: '确定',
  301. success: (res) => {
  302. if (res.confirm) {
  303. showDetailDialog.value = false
  304. handleDelete(index)
  305. }
  306. }
  307. })
  308. } else {
  309. showDetailDialog.value = false
  310. handleDelete(index)
  311. }
  312. }
  313. function handleClear() {
  314. clearStorage()
  315. refreshList()
  316. isEditing.value = false
  317. editIndex.value = -1
  318. editKey.value = ''
  319. editValue.value = ''
  320. }
  321. function confirmClear() {
  322. // 自动化测试时不显示模态弹窗
  323. if (!isTestMode.value) {
  324. uni.showModal({
  325. title: '确认操作',
  326. content: '确定要清空所有数据吗?',
  327. showCancel: true,
  328. cancelText: '取消',
  329. confirmText: '确定',
  330. success: (res) => {
  331. if (res.confirm) {
  332. showDetailDialog.value = false
  333. handleClear()
  334. }
  335. }
  336. })
  337. } else {
  338. showDetailDialog.value = false
  339. handleClear()
  340. }
  341. }
  342. function getDetailIndex() : number {
  343. return storageList.value.findIndex(item => item.key === detailItem.value.key)
  344. }
  345. function onValueTypeChange(e : UniRadioGroupChangeEvent) {
  346. const type = e.detail.value
  347. editValueType.value = type
  348. if (valueTypeDefaultMap.has(type)) {
  349. editValue.value = valueTypeDefaultMap.get(type) ?? ''
  350. } else {
  351. editValue.value = ''
  352. }
  353. }
  354. onLoad(() => {
  355. refreshList()
  356. })
  357. /**
  358. * 仅供自动化测试用例调用,设置测试模式
  359. */
  360. function setTestMode(val: boolean) {
  361. isTestMode.value = val
  362. }
  363. defineExpose({
  364. editValue,
  365. editValueType,
  366. getStorageList,
  367. setTestMode, // 仅供自动化测试用例调用
  368. })
  369. </script>
  370. <style>
  371. .container {
  372. flex-direction: column;
  373. background: #f7f8fa;
  374. flex: 1;
  375. }
  376. .header {
  377. flex-direction: row;
  378. align-items: center;
  379. justify-content: space-between;
  380. padding: 10px;
  381. }
  382. .btn {
  383. height: 40px;
  384. font-size: 16px;
  385. }
  386. .mr-20 {
  387. margin-right: 20px;
  388. }
  389. .list-view {
  390. flex: 1;
  391. width: 100%;
  392. background: #fff;
  393. border-radius: 0;
  394. padding: 0;
  395. }
  396. .item-block {
  397. padding: 13px;
  398. border-bottom: 1px solid #e5e5e5;
  399. background: #fff;
  400. }
  401. .item-row {
  402. flex-direction: row;
  403. align-items: center;
  404. margin-bottom: 4px;
  405. }
  406. .item-label {
  407. color: #888;
  408. font-size: 14px;
  409. margin-right: 4px;
  410. width: 50px;
  411. }
  412. .item-key {
  413. color: #333;
  414. font-size: 15px;
  415. flex: 1;
  416. /* #ifdef WEB */
  417. word-break: break-all;
  418. /* #endif */
  419. }
  420. .item-actions-row {
  421. justify-content: space-between;
  422. margin-top: 6px;
  423. margin-bottom: 0;
  424. }
  425. .dialog-mask {
  426. position: fixed;
  427. left: 0;
  428. top: 0;
  429. right: 0;
  430. bottom: 0;
  431. background: rgba(0, 0, 0, 0.18);
  432. display: flex;
  433. align-items: center;
  434. justify-content: center;
  435. z-index: 999;
  436. }
  437. .dialog-content {
  438. background: #fff;
  439. border-radius: 10px;
  440. padding: 20px 16px 16px 16px;
  441. min-width: 270px;
  442. max-width: 345px;
  443. flex-direction: column;
  444. align-items: stretch;
  445. position: relative;
  446. box-shadow: none;
  447. }
  448. .dialog-title {
  449. font-size: 16px;
  450. font-weight: bold;
  451. margin-bottom: 14px;
  452. text-align: center;
  453. color: #222;
  454. letter-spacing: 1px;
  455. }
  456. .detail-row {
  457. flex-direction: row;
  458. align-items: flex-start;
  459. margin-bottom: 9px;
  460. }
  461. .popup-actions {
  462. flex-direction: row;
  463. justify-content: flex-end;
  464. margin-top: 16px;
  465. margin-right: 8px;
  466. }
  467. .error-tip {
  468. color: #FF3B30;
  469. font-size: 15px;
  470. margin-bottom: 8px;
  471. text-align: center;
  472. font-weight: bold;
  473. }
  474. .edit-row {
  475. flex-direction: row;
  476. align-items: center;
  477. margin-bottom: 9px;
  478. }
  479. .edit-label {
  480. min-width: 35px;
  481. color: #888;
  482. font-size: 15px;
  483. margin-right: 6px;
  484. }
  485. .edit-input {
  486. flex: 1;
  487. height: 40px;
  488. border: 1px solid #ccc;
  489. border-radius: 4px;
  490. padding: 0 6px;
  491. font-size: 15px;
  492. background: #fff;
  493. }
  494. .edit-textarea {
  495. flex: 1;
  496. min-height: 80px;
  497. border: 1px solid #ccc;
  498. border-radius: 4px;
  499. padding: 10px 6px;
  500. font-size: 15px;
  501. background: #fff;
  502. }
  503. .edit-type-group {
  504. display: flex;
  505. flex-direction: row;
  506. align-items: center;
  507. flex-wrap: wrap;
  508. width: 90%;
  509. }
  510. .edit-type-radio {
  511. margin-right: 12px;
  512. margin-bottom: 6px;
  513. }
  514. </style>