Button.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. <template>
  2. <Touchable
  3. :innerStyle="{
  4. ...currentStyle.style,
  5. ...innerStyle,
  6. }"
  7. :innerClass="['nana-button', props.block ? 'nana-button-block' : 'nana-button-auto']"
  8. center
  9. direction="row"
  10. v-bind="viewProps"
  11. :pressedColor="finalPressedColor"
  12. :touchable="touchable && !loading"
  13. :gap="iconMargin"
  14. @state="(v) => state = v"
  15. @click="(e) => emit('click', e)"
  16. >
  17. <slot name="leftIcon">
  18. <ActivityIndicator
  19. v-if="loading"
  20. :size="selectStyleType(size, 'medium', FonstSizes)"
  21. :color="themeContext.resolveThemeColor(loadingColor) || currentStyle.color"
  22. />
  23. <Icon
  24. v-else-if="icon"
  25. :icon="icon"
  26. :size="selectStyleType(size, 'medium', FonstSizes)"
  27. :color="currentStyle.color"
  28. v-bind="iconProps"
  29. />
  30. </slot>
  31. <slot>
  32. <Text
  33. :color="textColorFinal"
  34. :fontSize="selectStyleType(size, 'medium', FonstSizes)"
  35. :fontWeight="type === 'text' ? 'bold' : undefined"
  36. :innerStyle="textStyle"
  37. :text="currentText"
  38. />
  39. </slot>
  40. <slot name="rightIcon">
  41. <Icon
  42. v-if="rightIcon"
  43. :icon="rightIcon"
  44. :size="selectStyleType(size, 'medium', FonstSizes)"
  45. :color="currentStyle.color"
  46. v-bind="rightIconProps"
  47. />
  48. </slot>
  49. </Touchable>
  50. </template>
  51. <script setup lang="ts">
  52. import { computed, inject, ref } from 'vue';
  53. import { propGetThemeVar, useTheme, type ViewStyle } from '../theme/ThemeDefine';
  54. import { configPadding, DynamicColor, DynamicSize, selectStyleType } from '../theme/ThemeTools';
  55. import type { IconProps } from './Icon.vue';
  56. import type { FlexProps } from '../layout/FlexView.vue';
  57. import Text from './Text.vue';
  58. import ActivityIndicator from './ActivityIndicator.vue';
  59. import Icon from './Icon.vue';
  60. import Touchable from '../feedback/Touchable.vue';
  61. import type { ButtonGroupContext } from './ButtonGroup.vue';
  62. import { useChildLinkChild } from '../composeabe/ChildItem';
  63. export type ButtonType = 'default'|'primary'|'success'|'warning'|'danger'|'custom'|'text';
  64. export type ButtomSizeType = 'small'|'medium'|'large'|'larger'|'mini';
  65. export interface ButtonProp {
  66. /**
  67. * 按钮文字
  68. */
  69. text?: string,
  70. /**
  71. * 按钮支持 default、primary、success、warning、danger、custom 自定义 六种类型
  72. * @default 'default'
  73. */
  74. type?: ButtonType,
  75. /**
  76. * 占满父级主轴
  77. * @default false
  78. */
  79. block?: boolean,
  80. /**
  81. * * plain 将按钮设置为朴素按钮,朴素按钮的文字为按钮颜色,背景为白色。
  82. * * light 将按钮设置为浅色按钮,浅色按钮的文字为按钮颜色,背景为主色调浅色。
  83. * @default 'default'
  84. */
  85. scheme?: 'default'|'plain'|'light',
  86. /**
  87. * 通过 loading 属性设置按钮为加载状态,加载状态下默认会隐藏按钮文字,可以通过 loadingText 设置加载状态下的文字。
  88. * @default false
  89. */
  90. loading?: boolean,
  91. /**
  92. * 加载状态下的文字。
  93. * @default false
  94. */
  95. loadingText?: string,
  96. /**
  97. * 加载状态圆圈颜色
  98. */
  99. loadingColor?: string,
  100. /**
  101. * 按钮形状 通过 square 设置方形按钮,通过 round 设置圆形按钮。
  102. * @default 'round'
  103. */
  104. shape?: 'square'|'round',
  105. /**
  106. * 左侧图标。支持 Icon 组件里的所有图标,也可以传入图标的图片 URL(http/https)。
  107. */
  108. icon?: string,
  109. /**
  110. * 当使用图标时,左侧图标的附加属性
  111. */
  112. iconProps?: IconProps;
  113. /**
  114. * 图标与文字之间的间距
  115. * @default 10
  116. */
  117. iconMargin?: number|string;
  118. /**
  119. * 右侧图标。支持 Icon 组件里的所有图标,也可以传入图标的图片 URL(http/https)。
  120. */
  121. rightIcon?: string,
  122. /**
  123. * 当使用图标时,右侧图标的附加属性
  124. */
  125. rightIconProps?: IconProps;
  126. /**
  127. * 是否可以点击
  128. * @default true
  129. */
  130. touchable?: boolean,
  131. /**
  132. * 当按扭为round圆形按扭时的圆角大小。
  133. * @default 5
  134. */
  135. radius?: number|string,
  136. /**
  137. * 按钮尺寸. 支持 large、medium、small、mini 四种尺寸。
  138. * @default 'medium'
  139. */
  140. size?: ButtomSizeType,
  141. /**
  142. * 通过 color 属性可以自定义按钮的背景颜色,仅在 type 为 `custom` 时有效
  143. * @default grey
  144. */
  145. color?: string;
  146. /**
  147. * 按钮文字的颜色。
  148. */
  149. textColor?: string;
  150. /**
  151. * 按下时按钮文字的颜色。
  152. */
  153. pressedTextColor?: string;
  154. /**
  155. * 按钮文字的样式。
  156. */
  157. textStyle?: object;
  158. /**
  159. * 按下时的颜色,仅在 type 为 `custom` 时有效
  160. * @default PressedColor(primary)
  161. */
  162. pressedColor?: string;
  163. /**
  164. * 禁用时的颜色,仅在 type 为 `custom` 时有效
  165. * @default grey
  166. */
  167. disabledColor?: string;
  168. /**
  169. * 自定义样式
  170. */
  171. innerStyle?: object,
  172. /**
  173. * 按扭的文字,等同于 text 属性
  174. */
  175. children?: string;
  176. /**
  177. * 强制控制按钮的边距
  178. * * 如果是数字,则设置所有方向边距
  179. * * 两位数组 [vetical,horizontal]
  180. * * 四位数组 [top,right,down,left]
  181. */
  182. padding?: number|number[],
  183. /**
  184. * 外层容器参数
  185. */
  186. viewProps?: FlexProps,
  187. formType?: string;
  188. openType?: string;
  189. appParameter?: string;
  190. }
  191. defineOptions({
  192. options: {
  193. styleIsolation: "shared",
  194. virtualHost: true,
  195. }
  196. })
  197. const emit = defineEmits([
  198. 'click',
  199. ])
  200. const themeContext = useTheme();
  201. const props = withDefaults(defineProps<ButtonProp>(), {
  202. touchable: true,
  203. loading: false,
  204. color: () => propGetThemeVar('ButtonColor', 'primary'),
  205. pressedColor: () => propGetThemeVar('ButtonPressedColor', 'pressed.primary'),
  206. disabledColor: () => propGetThemeVar('ButtonDisabledColor', 'grey'),
  207. type: () => propGetThemeVar('ButtonType', 'default'),
  208. size: () => propGetThemeVar('ButtonSize', 'medium'),
  209. block: () => propGetThemeVar('ButtonBlock', false),
  210. radius: () => propGetThemeVar('ButtonRadius', 16),
  211. iconMargin: () => propGetThemeVar('ButtonIconMargin', 10),
  212. shape: () => propGetThemeVar('ButtonShape', "round"),
  213. });
  214. const FonstSizes = computed(() => ({
  215. mini: themeContext.resolveThemeSize('ButtonMiniFonstSize', 'fontSize.xs'),
  216. small: themeContext.resolveThemeSize('ButtonSmallFonstSize', 'fontSize.sm'),
  217. medium: themeContext.resolveThemeSize('ButtonMediumFonstSize', 'fontSize.md'),
  218. large: themeContext.resolveThemeSize('ButtonLargeFonstSize', 'fontSize.lg'),
  219. larger: themeContext.resolveThemeSize('ButtonLargerFonstSize', 'fontSize.xl'),
  220. }));
  221. const themeVars = themeContext.getVars({
  222. ButtonBorderWidth: 1.5,
  223. ButtonDisableOpacity: 0.5,
  224. });
  225. const themeStyles = themeContext.useThemeStyles({
  226. plainButtonDefault: {
  227. borderStyle: 'solid',
  228. borderWidth: DynamicSize('ButtonBorderWidth', 1.5),
  229. borderColor: DynamicColor('ButtonPlainDefaultBorderColor', 'border.default'),
  230. color: DynamicColor('ButtonPlainDefaultColor', 'text.content'),
  231. },
  232. plainButtonPrimary: {
  233. borderStyle: 'solid',
  234. borderWidth: DynamicSize('ButtonBorderWidth', 1.5),
  235. borderColor: DynamicColor('ButtonPlainPrimaryBorderColor', 'primary'),
  236. color: DynamicColor('ButtonPlainPrimaryColor', 'primary'),
  237. },
  238. plainButtonSuccess: {
  239. borderStyle: 'solid',
  240. borderWidth: DynamicSize('ButtonBorderWidth', 1.5),
  241. borderColor: DynamicColor('ButtonPlainSuccessBorderColor', 'success'),
  242. color: DynamicColor('ButtonPlainSuccessColor', 'success'),
  243. },
  244. plainButtonWarning: {
  245. borderStyle: 'solid',
  246. borderWidth: DynamicSize('ButtonBorderWidth', 1.5),
  247. borderColor: DynamicColor('ButtonPlainWarningBorderColor', 'warning'),
  248. color: DynamicColor('ButtonPlainWarningColor', 'warning'),
  249. },
  250. plainButtonDanger: {
  251. borderStyle: 'solid',
  252. borderWidth: DynamicSize('ButtonBorderWidth', 1.5),
  253. borderColor: DynamicColor('ButtonPlainDangerBorderColor', 'danger'),
  254. color: DynamicColor('ButtonPlainDangerColor', 'danger'),
  255. },
  256. lightButtonDefault: {
  257. backgroundColor: DynamicColor('ButtonLightDefaultBackgroundColor', 'background.button'),
  258. color: DynamicColor('ButtonLightDefaultColor', 'text.content'),
  259. },
  260. lightButtonPrimary: {
  261. backgroundColor: DynamicColor('ButtonLightPrimaryBackgroundColor', 'background.primary'),
  262. color: DynamicColor('ButtonLightPrimaryColor', 'text.primary'),
  263. },
  264. lightButtonSuccess: {
  265. backgroundColor: DynamicColor('ButtonLightSuccessBackgroundColor', 'background.success'),
  266. color: DynamicColor('ButtonLightSuccessColor', 'text.success'),
  267. },
  268. lightButtonWarning: {
  269. backgroundColor: DynamicColor('ButtonLightWarningBackgroundColor', 'background.warning'),
  270. color: DynamicColor('ButtonLightWarningColor', 'text.warning'),
  271. },
  272. lightButtonDanger: {
  273. backgroundColor: DynamicColor('ButtonLightDangerBackgroundColor', 'background.danger'),
  274. color: DynamicColor('ButtonLightDangerColor', 'text.danger'),
  275. },
  276. buttonSizeLarger: {
  277. paddingVertical: DynamicSize('ButtonPaddingVerticalLarger', 25),
  278. paddingHorizontal: DynamicSize('ButtonPaddingHorizontalLarger', 30),
  279. },
  280. buttonSizeLarge: {
  281. paddingVertical: DynamicSize('ButtonPaddingVerticalLarge', 20),
  282. paddingHorizontal: DynamicSize('ButtonPaddingHorizontalLarge', 25),
  283. },
  284. buttonSizeMedium: {
  285. paddingVertical: DynamicSize('ButtonPaddingVerticalMedium', 15),
  286. paddingHorizontal: DynamicSize('ButtonPaddingHorizontalMedium', 20),
  287. },
  288. buttonSizeSmall: {
  289. paddingVertical: DynamicSize('ButtonPaddingVerticalSmall', 10),
  290. paddingHorizontal: DynamicSize('ButtonPaddingHorizontalSmall', 15),
  291. },
  292. buttonSizeMini: {
  293. paddingVertical: DynamicSize('ButtonPaddingVerticalMini', 5),
  294. paddingHorizontal: DynamicSize('ButtonPaddingHorizontalMini', 6),
  295. },
  296. buttonDefault: {
  297. backgroundColor: DynamicColor('ButtonDefaultBackgroundColor', 'button'),
  298. color: DynamicColor('ButtonDefaultColor', 'black'),
  299. },
  300. buttonPrimary: {
  301. backgroundColor: DynamicColor('ButtonPrimaryBackgroundColor', 'primary'),
  302. color: DynamicColor('ButtonPrimaryColor', 'white'),
  303. },
  304. buttonSuccess: {
  305. backgroundColor: DynamicColor('ButtonSuccessBackgroundColor', 'success'),
  306. color: DynamicColor('ButtonSuccessColor', 'white'),
  307. },
  308. buttonWarning: {
  309. backgroundColor: DynamicColor('ButtonWarningBackgroundColor', 'warning'),
  310. color: DynamicColor('ButtonWarningColor', 'white'),
  311. },
  312. buttonDanger: {
  313. backgroundColor: DynamicColor('ButtonDangerBackgroundColor', 'danger'),
  314. color: DynamicColor('ButtonDangerColor', 'white'),
  315. },
  316. });
  317. //按钮样式生成
  318. const currentStyle = computed(() => {
  319. const colorStyle = selectStyleType<ViewStyle, ButtonType>(props.type, 'default',
  320. selectStyleType(props.scheme, 'default', {
  321. default: {
  322. default: themeStyles.buttonDefault.value,
  323. primary: themeStyles.buttonPrimary.value,
  324. success: themeStyles.buttonSuccess.value,
  325. warning: themeStyles.buttonWarning.value,
  326. danger: themeStyles.buttonDanger.value,
  327. custom: {
  328. backgroundColor: themeContext.resolveThemeColor(touchable.value ? props.color : props.disabledColor),
  329. color: themeContext.resolveThemeColor(props.textColor),
  330. },
  331. text: {
  332. color: themeContext.resolveThemeColor(props.textColor),
  333. },
  334. },
  335. plain: {
  336. default: themeStyles.plainButtonDefault.value,
  337. primary: themeStyles.plainButtonPrimary.value,
  338. success: themeStyles.plainButtonSuccess.value,
  339. warning: themeStyles.plainButtonWarning.value,
  340. danger: themeStyles.plainButtonDanger.value,
  341. custom: {
  342. borderStyle: 'solid',
  343. borderWidth: themeContext.resolveThemeSize(themeVars.ButtonBorderWidth),
  344. borderColor: themeContext.resolveThemeColor(props.color),
  345. color: themeContext.resolveThemeColor(props.color),
  346. },
  347. text: {
  348. color: themeContext.resolveThemeColor(props.color),
  349. },
  350. },
  351. light: {
  352. default: themeStyles.lightButtonDefault.value,
  353. primary: themeStyles.lightButtonPrimary.value,
  354. success: themeStyles.lightButtonSuccess.value,
  355. warning: themeStyles.lightButtonWarning.value,
  356. danger: themeStyles.lightButtonDanger.value,
  357. custom: {
  358. backgroundColor: themeContext.resolveThemeColor(touchable.value ? props.color : props.disabledColor),
  359. color: themeContext.resolveThemeColor(props.textColor),
  360. },
  361. text: {
  362. color: themeContext.resolveThemeColor(props.color),
  363. },
  364. },
  365. })
  366. );
  367. const speicalStyle : ViewStyle = {
  368. opacity: touchable.value ? 1 : themeVars.ButtonDisableOpacity,
  369. borderRadius: props.shape === 'round' ? themeContext.resolveThemeSize(props.radius) : 0,
  370. };
  371. if (props.shape === 'round') {
  372. if (noLeftRadius.value) {
  373. speicalStyle.borderTopLeftRadius = 0;
  374. speicalStyle.borderBottomLeftRadius = 0;
  375. }
  376. if (noRightRadius.value) {
  377. speicalStyle.borderTopRightRadius = 0;
  378. speicalStyle.borderBottomRightRadius = 0;
  379. }
  380. }
  381. //自定义状态下的禁用颜色
  382. if (props.disabledColor && !touchable.value && props.type === 'custom')
  383. speicalStyle.backgroundColor = themeContext.resolveThemeColor(props.disabledColor);
  384. const sizeStyle = selectStyleType<ViewStyle, ButtomSizeType>(props.size, 'medium', {
  385. large: themeStyles.buttonSizeLarge.value,
  386. larger: themeStyles.buttonSizeLarger.value,
  387. medium: themeStyles.buttonSizeMedium.value,
  388. small: themeStyles.buttonSizeSmall.value,
  389. mini: themeStyles.buttonSizeMini.value,
  390. });
  391. //if (props.shape === 'round')
  392. // sizeStyle.paddingHorizontal = `calc(${sizeStyle.paddingHorizontal} + ${themeContext.resolveSize(props.radius / 4)})`;
  393. //内边距样式的强制设置
  394. configPadding(speicalStyle, themeContext, props.padding);
  395. return {
  396. color: (colorStyle).color,
  397. style: {
  398. ...colorStyle,
  399. ...sizeStyle,
  400. ...speicalStyle,
  401. },
  402. };
  403. });
  404. const state = ref('');
  405. const currentText = computed(() => (props.loading ? (props.loadingText || props.text) : props.text));
  406. const textColorFinal = computed(() => (
  407. state.value === 'active' ?
  408. themeContext.resolveThemeColor(props.pressedTextColor) :
  409. themeContext.resolveThemeColor(props.textColor)
  410. ) || currentStyle.value.color);
  411. const finalPressedColor = computed(() => {
  412. if (props.type === 'custom')
  413. return themeContext.resolveThemeColor(props.pressedColor);
  414. return (themeContext.resolveThemeColor((
  415. props.scheme === 'plain'
  416. || props.scheme === 'light'
  417. || props.type === 'text'
  418. ) ?
  419. 'pressed.notice' :
  420. 'pressed.' + props.type))
  421. });
  422. //按钮组的处理
  423. const buttonGroupContext = inject<ButtonGroupContext>('buttonGroupContext', undefined as any);
  424. const { position } = useChildLinkChild(() => buttonGroupContext?.getPosition());
  425. const noRightRadius = computed(() => buttonGroupContext?.mergeRadius.value && position.value !== buttonGroupContext.length.value - 1);
  426. const noLeftRadius = computed(() => buttonGroupContext?.mergeRadius.value && position.value !== 0);
  427. const touchable = computed(() => props.touchable !== false && !buttonGroupContext?.groupDisabled.value);
  428. </script>
  429. <style>
  430. .nana-button {
  431. width: auto;
  432. user-select: none;
  433. cursor: pointer;
  434. appearance: none;
  435. }
  436. .nana-button-inner {
  437. display: flex;
  438. flex-direction: row;
  439. justify-content: center;
  440. align-items: center;
  441. background-color: transparent;
  442. border: none;
  443. outline: none;
  444. line-height: auto;
  445. padding: 0;
  446. margin: 0;
  447. min-height: 0;
  448. }
  449. .nana-button-inner::after {
  450. display: none;
  451. }
  452. .nana-button-auto {
  453. flex-shrink: 0;
  454. flex-grow: 0;
  455. flex-basis: auto;
  456. }
  457. .nana-button-block {
  458. align-self: stretch;
  459. max-width: 100%;
  460. }
  461. </style>