diff --git a/src/components/DictResolver/index.js b/src/components/DictResolver/index.js new file mode 100644 index 0000000000000000000000000000000000000000..6321c1b448f93134b22e0956f26546d648d0eefd --- /dev/null +++ b/src/components/DictResolver/index.js @@ -0,0 +1,21 @@ +import Vue from 'vue' +import DataDict from '@/util/dict' +import {remote} from '@/api/admin/dict' + +function install() { + Vue.use(DataDict, { + metas: { + '*': { + labelField: 'label', + valueField: 'value', + request(dictMeta) { + return remote(dictMeta.type).then(res => res.data) + }, + }, + }, + }) +} + +export default { + install, +} diff --git a/src/util/dict/Dict.js b/src/util/dict/Dict.js new file mode 100644 index 0000000000000000000000000000000000000000..782942c5d2842070e56f2d022dd28c325a6cf501 --- /dev/null +++ b/src/util/dict/Dict.js @@ -0,0 +1,71 @@ +import Vue from 'vue' +import { mergeRecursive } from "@/util/util"; +import DictMeta from './DictMeta' +import DictData from './DictData' + +const DEFAULT_DICT_OPTIONS = { + types: [], +} + +/** + * @classdesc 字典 + * @property {Object} label 标签对象,内部属性名为字典类型名称 + * @property {Object} dict 字段数组,内部属性名为字典类型名称 + * @property {Array.} _dictMetas 字典元数据数组 + */ +export default class Dict { + constructor() { + this.owner = null + this.label = {} + this.type = {} + } + + init(options) { + if (options instanceof Array) { + options = { types: options } + } + const opts = mergeRecursive(DEFAULT_DICT_OPTIONS, options) + if (opts.types === undefined) { + throw new Error('字典类型为空') + } + const ps = [] + this._dictMetas = opts.types.map(t => DictMeta.parse(t)) + this._dictMetas.forEach(dictMeta => { + const type = dictMeta.type + Vue.set(this.label, type, {}) + Vue.set(this.type, type, []) + if (dictMeta.lazy) { + return + } + ps.push(loadDict(this, dictMeta)) + }) + return Promise.all(ps) + } + +} + +/** + * 加载字典 + * @param {Dict} dict 字典 + * @param {DictMeta} dictMeta 字典元数据 + * @returns {Promise} + */ +function loadDict(dict, dictMeta) { + return dictMeta.request(dictMeta) + .then(response => { + const type = dictMeta.type + let dicts = dictMeta.responseConverter(response, dictMeta) + if (!(dicts instanceof Array)) { + console.error('the return of responseConverter must be Array.') + dicts = [] + } else if (dicts.filter(d => d instanceof DictData).length !== dicts.length) { + console.error('the type of elements in dicts must be DictResolver') + dicts = [] + } + dict.type[type].splice(0, Number.MAX_SAFE_INTEGER, ...dicts) + dicts.forEach(d => { + Vue.set(dict.label[type], d.value, d.label) + }) + return dicts + }) +} diff --git a/src/util/dict/DictConverter.js b/src/util/dict/DictConverter.js new file mode 100644 index 0000000000000000000000000000000000000000..0cf5df8636449bb8d5be54d7850aae2aab1a8520 --- /dev/null +++ b/src/util/dict/DictConverter.js @@ -0,0 +1,17 @@ +import DictOptions from './DictOptions' +import DictData from './DictData' + +export default function(dict, dictMeta) { + const label = determineDictField(dict, dictMeta.labelField, ...DictOptions.DEFAULT_LABEL_FIELDS) + const value = determineDictField(dict, dictMeta.valueField, ...DictOptions.DEFAULT_VALUE_FIELDS) + return new DictData(dict[label], dict[value], dict) +} + +/** + * 确定字典字段 + * @param {DictData} dict + * @param {...String} fields + */ +function determineDictField(dict, ...fields) { + return fields.find(f => Object.prototype.hasOwnProperty.call(dict, f)) +} diff --git a/src/util/dict/DictData.js b/src/util/dict/DictData.js new file mode 100644 index 0000000000000000000000000000000000000000..afc763e8017f76226b5c281ad3b1bbdb82d3da84 --- /dev/null +++ b/src/util/dict/DictData.js @@ -0,0 +1,13 @@ +/** + * @classdesc 字典数据 + * @property {String} label 标签 + * @property {*} value 标签 + * @property {Object} raw 原始数据 + */ +export default class DictData { + constructor(label, value, raw) { + this.label = label + this.value = value + this.raw = raw + } +} diff --git a/src/util/dict/DictMeta.js b/src/util/dict/DictMeta.js new file mode 100644 index 0000000000000000000000000000000000000000..98f687e31203d0d788554a02a0c777984073530c --- /dev/null +++ b/src/util/dict/DictMeta.js @@ -0,0 +1,38 @@ +import { mergeRecursive } from "@/util/util"; +import DictOptions from './DictOptions' + +/** + * @classdesc 字典元数据 + * @property {String} type 类型 + * @property {Function} request 请求 + * @property {String} label 标签字段 + * @property {String} value 值字段 + */ +export default class DictMeta { + constructor(options) { + this.type = options.type + this.request = options.request + this.responseConverter = options.responseConverter + this.labelField = options.labelField + this.valueField = options.valueField + this.lazy = options.lazy === true + } +} + + +/** + * 解析字典元数据 + * @param {Object} options + * @returns {DictMeta} + */ +DictMeta.parse= function(options) { + let opts = null + if (typeof options === 'string') { + opts = DictOptions.metas[options] || {} + opts.type = options + } else if (typeof options === 'object') { + opts = options + } + opts = mergeRecursive(DictOptions.metas['*'], opts) + return new DictMeta(opts) +} diff --git a/src/util/dict/DictOptions.js b/src/util/dict/DictOptions.js new file mode 100644 index 0000000000000000000000000000000000000000..4730e477489304fcc37c7b572fedd348fef2eca4 --- /dev/null +++ b/src/util/dict/DictOptions.js @@ -0,0 +1,51 @@ +import {mergeRecursive} from "@/util/util"; +import dictConverter from './DictConverter' + +export const options = { + metas: { + '*': { + /** + * 字典请求,方法签名为function(dictMeta: DictMeta): Promise + */ + request: (dictMeta) => { + console.log(`load dict ${dictMeta.type}`) + return Promise.resolve([]) + }, + /** + * 字典响应数据转换器,方法签名为function(response: Object, dictMeta: DictMeta): DictResolver + */ + responseConverter, + labelField: 'label', + valueField: 'value', + }, + }, + /** + * 默认标签字段 + */ + DEFAULT_LABEL_FIELDS: ['label', 'name', 'title'], + /** + * 默认值字段 + */ + DEFAULT_VALUE_FIELDS: ['value', 'id', 'uid', 'key'], +} + +/** + * 映射字典 + * @param {Object} response 字典数据 + * @param {DictMeta} dictMeta 字典元数据 + * @returns {DictData} + */ +function responseConverter(response, dictMeta) { + const dicts = response.data instanceof Array ? response.data : response + if (dicts === undefined) { + console.warn(`no dict data of "${dictMeta.type}" found in the response`) + return [] + } + return dicts.map(d => dictConverter(d, dictMeta)) +} + +export function mergeOptions(src) { + mergeRecursive(options, src) +} + +export default options diff --git a/src/util/dict/index.js b/src/util/dict/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e3c601ea2a400830e8c8ec9d7e2d7de3bed552c7 --- /dev/null +++ b/src/util/dict/index.js @@ -0,0 +1,33 @@ +import Dict from './Dict' +import {mergeOptions} from './DictOptions' + +export default function (Vue, options) { + mergeOptions(options) + Vue.mixin({ + data() { + if (this.$options === undefined || this.$options.dictTypes === undefined || this.$options.dictTypes === null) { + return {} + } + const dict = new Dict() + dict.owner = this + return { + dict + } + }, + created() { + if (!(this.dict instanceof Dict)) { + return + } + options.onCreated && options.onCreated(this.dict) + this.dict.init(this.$options.dictTypes).then(() => { + options.onReady && options.onReady(this.dict) + this.$nextTick(() => { + this.$emit('dictReady', this.dict) + if (this.$options.methods && this.$options.methods.onDictReady instanceof Function) { + this.$options.methods.onDictReady.call(this, this.dict) + } + }) + }) + }, + }) +} diff --git a/src/util/util.js b/src/util/util.js index 1e2352b06e4b1c829f65fd47fc2e5da4be877616..487310d8fa0501dbfda2fce5fa02d03c2cf7f475 100644 --- a/src/util/util.js +++ b/src/util/util.js @@ -1,328 +1,362 @@ -import {validatenull} from "./validate"; -import request from "@/router/axios"; -import * as CryptoJS from "crypto-js"; - -// 表单序列化 -export const serialize = data => { - let list = []; - Object.keys(data).forEach(ele => { - list.push(`${ele}=${data[ele]}`); - }); - return list.join("&"); -}; -export const getObjType = obj => { - var toString = Object.prototype.toString; - var map = { - "[object Boolean]": "boolean", - "[object Number]": "number", - "[object String]": "string", - "[object Function]": "function", - "[object Array]": "array", - "[object Date]": "date", - "[object RegExp]": "regExp", - "[object Undefined]": "undefined", - "[object Null]": "null", - "[object Object]": "object" - }; - if (obj instanceof Element) { - return "element"; - } - return map[toString.call(obj)]; -}; -/** - * 对象深拷贝 - */ -export const deepClone = data => { - var type = getObjType(data); - var obj; - if (type === "array") { - obj = []; - } else if (type === "object") { - obj = {}; - } else { - // 不再具有下一层次 - return data; - } - if (type === "array") { - for (var i = 0, len = data.length; i < len; i++) { - obj.push(deepClone(data[i])); - } - } else if (type === "object") { - for (var key in data) { - obj[key] = deepClone(data[key]); - } - } - return obj; -}; -/** - * 判断路由是否相等 - */ -export const diff = (obj1, obj2) => { - delete obj1.close; - var o1 = obj1 instanceof Object; - var o2 = obj2 instanceof Object; - if (!o1 || !o2) { - /* 判断不是对象 */ - return obj1 === obj2; - } - - if (Object.keys(obj1).length !== Object.keys(obj2).length) { - return false; - // Object.keys() 返回一个由对象的自身可枚举属性(key值)组成的数组,例如:数组返回下表:let arr = ["a", "b", "c"];console.log(Object.keys(arr))->0,1,2; - } - - for (var attr in obj1) { - var t1 = obj1[attr] instanceof Object; - var t2 = obj2[attr] instanceof Object; - if (t1 && t2) { - return diff(obj1[attr], obj2[attr]); - } else if (obj1[attr] !== obj2[attr]) { - return false; - } - } - return true; -}; -/** - * 设置灰度模式 - */ -export const toggleGrayMode = status => { - if (status) { - document.body.className = document.body.className + " grayMode"; - } else { - document.body.className = document.body.className.replace(" grayMode", ""); - } -}; -/** - * 设置主题 - */ -export const setTheme = name => { - document.body.className = name; -}; - -/** - *加密处理 - */ -export const encryption = params => { - let {data, type, param, key} = params; - const result = JSON.parse(JSON.stringify(data)); - if (type === "Base64") { - param.forEach(ele => { - result[ele] = btoa(result[ele]); - }); - } else { - param.forEach(ele => { - var data = result[ele]; - key = CryptoJS.enc.Latin1.parse(key); - var iv = key; - // 加密 - var encrypted = CryptoJS.AES.encrypt(data, key, { - iv: iv, - mode: CryptoJS.mode.CFB, - padding: CryptoJS.pad.NoPadding - }); - result[ele] = encrypted.toString(); - }); - } - return result; -}; -/** - * 浏览器判断是否全屏 - */ -export const fullscreenToggel = () => { - if (fullscreenEnable()) { - exitFullScreen(); - } else { - reqFullScreen(); - } -}; -/** - * esc监听全屏 - */ -export const listenfullscreen = callback => { - function listen() { - callback(); - } - - document.addEventListener("fullscreenchange", function () { - listen(); - }); - document.addEventListener("mozfullscreenchange", function () { - listen(); - }); - document.addEventListener("webkitfullscreenchange", function () { - listen(); - }); - document.addEventListener("msfullscreenchange", function () { - listen(); - }); -}; -/** - * 浏览器判断是否全屏 - */ -export const fullscreenEnable = () => { - return ( - document.isFullScreen || - document.mozIsFullScreen || - document.webkitIsFullScreen - ); -}; - -/** - * 浏览器全屏 - */ -export const reqFullScreen = () => { - if (document.documentElement.requestFullScreen) { - document.documentElement.requestFullScreen(); - } else if (document.documentElement.webkitRequestFullScreen) { - document.documentElement.webkitRequestFullScreen(); - } else if (document.documentElement.mozRequestFullScreen) { - document.documentElement.mozRequestFullScreen(); - } -}; -/** - * 浏览器退出全屏 - */ -export const exitFullScreen = () => { - if (document.documentElement.requestFullScreen) { - document.exitFullScreen(); - } else if (document.documentElement.webkitRequestFullScreen) { - document.webkitCancelFullScreen(); - } else if (document.documentElement.mozRequestFullScreen) { - document.mozCancelFullScreen(); - } -}; -/** - * 递归寻找子类的父类 - */ - -export const findParent = (menu, id) => { - for (let i = 0; i < menu.length; i++) { - if (menu[i].children.length != 0) { - for (let j = 0; j < menu[i].children.length; j++) { - if (menu[i].children[j].id == id) { - return menu[i]; - } else { - if (menu[i].children[j].children.length != 0) { - return findParent(menu[i].children[j].children, id); - } - } - } - } - } -}; - -/** - * 动态插入css - */ - -export const loadStyle = url => { - const link = document.createElement("link"); - link.type = "text/css"; - link.rel = "stylesheet"; - link.href = url; - const head = document.getElementsByTagName("head")[0]; - head.appendChild(link); -}; -/** - * 判断路由是否相等 - */ -export const isObjectValueEqual = (a, b) => { - let result = true; - Object.keys(a).forEach(ele => { - const type = typeof a[ele]; - if (type === "string" && a[ele] !== b[ele]) result = false; - else if ( - type === "object" && - JSON.stringify(a[ele]) !== JSON.stringify(b[ele]) - ) - result = false; - }); - return result; -}; -/** - * 根据字典的value显示label - */ -export const findByvalue = (dic, value) => { - let result = ""; - if (validatenull(dic)) return value; - if ( - typeof value === "string" || - typeof value === "number" || - typeof value === "boolean" - ) { - let index = 0; - index = findArray(dic, value); - if (index != -1) { - result = dic[index].label; - } else { - result = value; - } - } else if (value instanceof Array) { - result = []; - let index = 0; - value.forEach(ele => { - index = findArray(dic, ele); - if (index != -1) { - result.push(dic[index].label); - } else { - result.push(value); - } - }); - result = result.toString(); - } - return result; -}; -/** - * 根据字典的value查找对应的index - */ -export const findArray = (dic, value) => { - for (let i = 0; i < dic.length; i++) { - if (dic[i].value == value) { - return i; - } - } - return -1; -}; -/** - * 生成随机len位数字 - */ -export const randomLenNum = (len, date) => { - let random = ""; - random = Math.ceil(Math.random() * 100000000000000) - .toString() - .substr(0, len || 4); - if (date) random = random + Date.now(); - return random; -}; - -/** - * - * @param url 目标下载接口 - * @param query 查询参数 - * @param fileName 文件名称 - * @returns {*} - */ -export function downBlobFile(url, query, fileName) { - return request({ - url: url, - method: "get", - responseType: "blob", - params: query - }).then(response => { - // 处理返回的文件流 - const blob = response.data; - if (blob && blob.size === 0) { - this.$notify.error("内容为空,无法下载"); - return; - } - const link = document.createElement("a"); - link.href = URL.createObjectURL(blob); - link.download = fileName; - document.body.appendChild(link); - link.click(); - window.setTimeout(function () { - URL.revokeObjectURL(blob); - document.body.removeChild(link); - }, 0); - }); -} +import {validatenull} from "./validate"; +import request from "@/router/axios"; +import * as CryptoJS from "crypto-js"; + +// 数据合并 +export function mergeRecursive(source, target) { + for (let index in target) { + try { + if (target[index].constructor === Object) { + source[index] = mergeRecursive(source[index], target[index]); + } else { + source[index] = target[index]; + } + } catch (e) { + source[index] = target[index]; + } + } + return source; +} + +// 回显数据字典 +export function selectDictLabel(datas, value) { + if (value === undefined) { + return ""; + } + let actions = []; + Object.keys(datas).some((key) => { + if (datas[key].value === ('' + value)) { + actions.push(datas[key].label); + return true; + } + }) + if (actions.length === 0) { + actions.push(value); + } + return actions.join(''); +} + +// 表单序列化 +export const serialize = data => { + let list = []; + Object.keys(data).forEach(ele => { + list.push(`${ele}=${data[ele]}`); + }); + return list.join("&"); +}; +export const getObjType = obj => { + var toString = Object.prototype.toString; + var map = { + "[object Boolean]": "boolean", + "[object Number]": "number", + "[object String]": "string", + "[object Function]": "function", + "[object Array]": "array", + "[object Date]": "date", + "[object RegExp]": "regExp", + "[object Undefined]": "undefined", + "[object Null]": "null", + "[object Object]": "object" + }; + if (obj instanceof Element) { + return "element"; + } + return map[toString.call(obj)]; +}; +/** + * 对象深拷贝 + */ +export const deepClone = data => { + var type = getObjType(data); + var obj; + if (type === "array") { + obj = []; + } else if (type === "object") { + obj = {}; + } else { + // 不再具有下一层次 + return data; + } + if (type === "array") { + for (var i = 0, len = data.length; i < len; i++) { + obj.push(deepClone(data[i])); + } + } else if (type === "object") { + for (var key in data) { + obj[key] = deepClone(data[key]); + } + } + return obj; +}; +/** + * 判断路由是否相等 + */ +export const diff = (obj1, obj2) => { + delete obj1.close; + var o1 = obj1 instanceof Object; + var o2 = obj2 instanceof Object; + if (!o1 || !o2) { + /* 判断不是对象 */ + return obj1 === obj2; + } + + if (Object.keys(obj1).length !== Object.keys(obj2).length) { + return false; + // Object.keys() 返回一个由对象的自身可枚举属性(key值)组成的数组,例如:数组返回下表:let arr = ["a", "b", "c"];console.log(Object.keys(arr))->0,1,2; + } + + for (var attr in obj1) { + var t1 = obj1[attr] instanceof Object; + var t2 = obj2[attr] instanceof Object; + if (t1 && t2) { + return diff(obj1[attr], obj2[attr]); + } else if (obj1[attr] !== obj2[attr]) { + return false; + } + } + return true; +}; +/** + * 设置灰度模式 + */ +export const toggleGrayMode = status => { + if (status) { + document.body.className = document.body.className + " grayMode"; + } else { + document.body.className = document.body.className.replace(" grayMode", ""); + } +}; +/** + * 设置主题 + */ +export const setTheme = name => { + document.body.className = name; +}; + +/** + *加密处理 + */ +export const encryption = params => { + let {data, type, param, key} = params; + const result = JSON.parse(JSON.stringify(data)); + if (type === "Base64") { + param.forEach(ele => { + result[ele] = btoa(result[ele]); + }); + } else { + param.forEach(ele => { + var data = result[ele]; + key = CryptoJS.enc.Latin1.parse(key); + var iv = key; + // 加密 + var encrypted = CryptoJS.AES.encrypt(data, key, { + iv: iv, + mode: CryptoJS.mode.CFB, + padding: CryptoJS.pad.NoPadding + }); + result[ele] = encrypted.toString(); + }); + } + return result; +}; +/** + * 浏览器判断是否全屏 + */ +export const fullscreenToggel = () => { + if (fullscreenEnable()) { + exitFullScreen(); + } else { + reqFullScreen(); + } +}; +/** + * esc监听全屏 + */ +export const listenfullscreen = callback => { + function listen() { + callback(); + } + + document.addEventListener("fullscreenchange", function () { + listen(); + }); + document.addEventListener("mozfullscreenchange", function () { + listen(); + }); + document.addEventListener("webkitfullscreenchange", function () { + listen(); + }); + document.addEventListener("msfullscreenchange", function () { + listen(); + }); +}; +/** + * 浏览器判断是否全屏 + */ +export const fullscreenEnable = () => { + return ( + document.isFullScreen || + document.mozIsFullScreen || + document.webkitIsFullScreen + ); +}; + +/** + * 浏览器全屏 + */ +export const reqFullScreen = () => { + if (document.documentElement.requestFullScreen) { + document.documentElement.requestFullScreen(); + } else if (document.documentElement.webkitRequestFullScreen) { + document.documentElement.webkitRequestFullScreen(); + } else if (document.documentElement.mozRequestFullScreen) { + document.documentElement.mozRequestFullScreen(); + } +}; +/** + * 浏览器退出全屏 + */ +export const exitFullScreen = () => { + if (document.documentElement.requestFullScreen) { + document.exitFullScreen(); + } else if (document.documentElement.webkitRequestFullScreen) { + document.webkitCancelFullScreen(); + } else if (document.documentElement.mozRequestFullScreen) { + document.mozCancelFullScreen(); + } +}; +/** + * 递归寻找子类的父类 + */ + +export const findParent = (menu, id) => { + for (let i = 0; i < menu.length; i++) { + if (menu[i].children.length != 0) { + for (let j = 0; j < menu[i].children.length; j++) { + if (menu[i].children[j].id == id) { + return menu[i]; + } else { + if (menu[i].children[j].children.length != 0) { + return findParent(menu[i].children[j].children, id); + } + } + } + } + } +}; + +/** + * 动态插入css + */ + +export const loadStyle = url => { + const link = document.createElement("link"); + link.type = "text/css"; + link.rel = "stylesheet"; + link.href = url; + const head = document.getElementsByTagName("head")[0]; + head.appendChild(link); +}; +/** + * 判断路由是否相等 + */ +export const isObjectValueEqual = (a, b) => { + let result = true; + Object.keys(a).forEach(ele => { + const type = typeof a[ele]; + if (type === "string" && a[ele] !== b[ele]) result = false; + else if ( + type === "object" && + JSON.stringify(a[ele]) !== JSON.stringify(b[ele]) + ) + result = false; + }); + return result; +}; +/** + * 根据字典的value显示label + */ +export const findByvalue = (dic, value) => { + let result = ""; + if (validatenull(dic)) return value; + if ( + typeof value === "string" || + typeof value === "number" || + typeof value === "boolean" + ) { + let index = 0; + index = findArray(dic, value); + if (index != -1) { + result = dic[index].label; + } else { + result = value; + } + } else if (value instanceof Array) { + result = []; + let index = 0; + value.forEach(ele => { + index = findArray(dic, ele); + if (index != -1) { + result.push(dic[index].label); + } else { + result.push(value); + } + }); + result = result.toString(); + } + return result; +}; +/** + * 根据字典的value查找对应的index + */ +export const findArray = (dic, value) => { + for (let i = 0; i < dic.length; i++) { + if (dic[i].value == value) { + return i; + } + } + return -1; +}; +/** + * 生成随机len位数字 + */ +export const randomLenNum = (len, date) => { + let random = ""; + random = Math.ceil(Math.random() * 100000000000000) + .toString() + .substr(0, len || 4); + if (date) random = random + Date.now(); + return random; +}; + +/** + * + * @param url 目标下载接口 + * @param query 查询参数 + * @param fileName 文件名称 + * @returns {*} + */ +export function downBlobFile(url, query, fileName) { + return request({ + url: url, + method: "get", + responseType: "blob", + params: query + }).then(response => { + // 处理返回的文件流 + const blob = response.data; + if (blob && blob.size === 0) { + this.$notify.error("内容为空,无法下载"); + return; + } + const link = document.createElement("a"); + link.href = URL.createObjectURL(blob); + link.download = fileName; + document.body.appendChild(link); + link.click(); + window.setTimeout(function () { + URL.revokeObjectURL(blob); + document.body.removeChild(link); + }, 0); + }); +} diff --git a/src/views/gen/datasource.vue b/src/views/gen/datasource.vue index 07607e5e2d2fcd0dd1b68f66d1ba7f100679c06e..b48b97e2f0dc16683cb384f478b05273b316efdc 100755 --- a/src/views/gen/datasource.vue +++ b/src/views/gen/datasource.vue @@ -93,13 +93,15 @@ handleSave: function (row, done,loading) { addObj(row).then(res => { if (res.data.data){ - done() - this.$message.success('添加成功') + this.$message.success("添加成功"); + done(); + this.getDsList(this.dsPage); }else { + this.$message.error("添加失败,数据源不可访问"); loading() - this.$message.error('添加失败,数据源不可访问') } - this.getDsList(this.dsPage) + }).catch(()=>{ + loading() }) }, getDsList(page, params) {